Introduction

Tyco (Typed Config) is a configuration file format designed for explicit typing, readability, and composability. Unlike formats that infer types from values, Tyco requires explicit type annotations, making configurations self-documenting and eliminating ambiguity.

Tyco is designed for:

Complete Example

Here is a complete example demonstrating many of Tyco's features:

# Global configuration
str environment: production
str region: us-east-1

# Database definitions
Database:
 *str name:
  str host:
  int port: 3306
  bool ssl: true
  - primary, db1.example.com
  - replica, db2.example.com, ssl: false

# Server definitions
Server:
 *str hostname:
  str ip:
  int cores:
  bool ssd: true
 ?str notes:
  - web-01, 10.0.1.10, 8
  - web-02, 10.0.1.11, 8
  - db-01, 10.0.2.10, 16, notes: "High memory instance"

# Application definitions
Application:
 *str name:
  str version:
  Server server:
  Database database:
  int[] ports:
  str url:
  - web-app, 2.1.0, Server(web-01), Database(primary), \
    ports: [80, 443], url: "https://{name}.{global.region}.example.com"
  - api, 1.5.3, Server(web-02), Database(replica), \
    ports: [8080], url: "https://{name}.{server.hostname}:8080"

Features

Tyco provides a rich set of features for building type-safe, maintainable configuration files:

Type System

Data Structures

References & Relations

String Features

Number Formats

Defaults & Reusability

Composition & Organization

Design Principles

Language Integration

Date & Time Support

Developer Experience

Specification

This document defines Tyco version 0.2.0. A Tyco file must be a valid UTF-8 encoded Unicode document.

Whitespace means tab (0x09) or space (0x20). Newline means LF (0x0A) or CRLF (0x0D 0x0A).

Comment

A hash symbol (#) marks the rest of the line as a comment, which is ignored by parsers.

# This is a full-line comment
str timezone: UTC  # This is an end-of-line comment

Comments cannot contain control characters other than tab (U+0009).

Basic Types

Tyco provides a comprehensive set of built-in data types for configuration values. Each type has specific syntax and validation rules:

All values in Tyco must explicitly declare their type, enabling strong type checking and eliminating ambiguity. Arrays can be created for any type by adding [] after the type name.

String

Strings are sequences of Unicode characters. Tyco supports both basic strings (with escape sequences) and literal strings (without escape processing).

Unquoted Strings

For the majority of configuration values, quotes are not required at all and can be safely omitted. Simple values like hostnames, names, and identifiers work perfectly without quotes:

str hostname: server1.example.com
str environment: production
str region: us-east-1

When to Quote Values

Use quoted strings when your content contains syntax characters that could be interpreted as delimiters or structural elements:

# URLs with colons, query parameters with commas
str api_url: 'https://api.example.com/search?q=python,java&limit=10'

# Descriptions with commas
str description: 'A fast, reliable, and secure service'

# Values with brackets, parentheses, or braces
str expression: 'func(arg1, arg2)'
str template: '{variable} with literal braces'

No quotes needed for: Simple hostnames, paths, version numbers, and times (since times start with digits and can't be confused with field names):

str hostname: api.example.com
str version: 1.2.3
str start_time: 14:30:00

Unquoted strings undergo template expansion (replacing {variable} references), while literal strings (using single quotes) disable this behavior.

Basic Strings

Basic strings are surrounded by double quotes ("). Any Unicode character may be used except those that must be escaped: backslash (\) and the double quote ("). Basic strings support template expansion.

str message: "Hello, world!"
str path: "C:\\Users\\name"

Escape sequences:

Escape Unicode Description
\bU+0008backspace
\tU+0009tab
\nU+000Alinefeed
\fU+000Cform feed
\rU+000Dcarriage return
\"U+0022quote
\\U+005Cbackslash
\uXXXXUnicode (4 hex digits)
\UXXXXXXXXUnicode (8 hex digits)

Multi-line Basic Strings

Multi-line basic strings are surrounded by three double quotes ("""). Newlines are allowed and will be preserved.

str poem: """
Roses are red
Violets are blue
Tyco is typed
And you're my type too"""

A backslash at the end of a line will trim all whitespace (including newlines) up to the next non-whitespace character or closing delimiter.

str long: """This is a \
    very long \
    sentence."""  # Renders as: "This is a very long sentence."

Literal Strings

Literal strings are surrounded by single quotes ('). No escape sequences are processed, and template expansion is disabled. What you see is what you get. Use literal strings when you need the exact text without any processing.

str winpath: 'C:\Users\name'  # Backslashes are literal
str regex: '\d{2}-\w+'
str template: '{variable}'  # No template expansion - literal {variable}

Multi-line Literal Strings

Multi-line literal strings are surrounded by three single quotes (''').

str lines: '''
First line
Second line'''

Integer

Integers are whole numbers. Positive integers may be prefixed with a plus (+). Negative integers are prefixed with a minus (-).

int count: 42
int negative: -17
int positive: +99

Integer Formats

Tyco supports hexadecimal, octal, and binary representations:

# Hexadecimal with prefix 0x
int hex: 0x1F  # 31 in decimal

# Octal with prefix 0o
int octal: 0o755  # 493 in decimal

# Binary with prefix 0b
int binary: 0b11010110  # 214 in decimal

Float

Floats are numbers with a fractional component. They must contain a decimal point.

float pi: 3.14159
float negative: -0.001
float version: 1.0

Decimal

Decimals are arbitrary-precision numbers with a fixed number of decimal places. Unlike floats, decimals avoid floating-point precision errors, making them ideal for financial calculations, currencies, and other applications requiring exact decimal arithmetic.

decimal price: 99.95
decimal tax_rate: 0.0825
decimal balance: -1234.56

Syntax

Decimal values use the same numeric syntax as floats but are interpreted with arbitrary precision. Both positive and negative values are supported.

decimal amount: 1234567.89
decimal precise: 0.123456789012345

Language Implementation

Parser implementations map decimal values to appropriate types in the target language:

Custom Decimal Class

For languages without built-in decimal support, parsers implement a lightweight decimal representation:

// Example JavaScript implementation
class Decimal {
  constructor(value, scale) {
    this.value = value;  // Integer representation
    this.scale = scale;  // Number of decimal places
  }
  
  toString() {
    const str = this.value.toString();
    if (this.scale === 0) return str;
    
    const negative = str.startsWith('-');
    const digits = negative ? str.slice(1) : str;
    const padded = digits.padStart(this.scale + 1, '0');
    const whole = padded.slice(0, -this.scale) || '0';
    const fraction = padded.slice(-this.scale);
    
    return (negative ? '-' : '') + whole + '.' + fraction;
  }
}

Note: Custom decimal implementations typically store only the representation (value and scale) and delegate arithmetic operations to external libraries or user code. This ensures accurate decimal representation without complex arithmetic implementation.

Boolean

Booleans are represented by the lowercase keywords true and false.

bool enabled: true
bool debug: false

Date & Time

Tyco supports date, time, and datetime values using ISO 8601 format.

Date

date birthday: 1990-05-15  # YYYY-MM-DD

Time

time alarm: 07:30:00  # HH:MM:SS

Datetime

Datetime values combine date and time components. The standard ISO 8601 format uses a "T" separator, but this can be omitted and replaced with a space for improved readability:

# Standard ISO 8601 format with T separator
datetime created: 2024-01-15T14:30:00
datetime updated: 2024-01-15T14:30:00Z  # With timezone

# Alternative format with space separator (more readable)
datetime started: 2024-01-15 14:30:00
datetime finished: 2024-01-15 14:30:00Z  # With timezone

Both formats are equivalent and supported by Tyco parsers. The space-separated format is often preferred in configuration files for better human readability.

Array

Arrays are collections of values enclosed in square brackets. Values are separated by commas. Trailing commas are allowed and can improve readability when arrays span multiple lines.

int[] ports: [80, 443, 8080]
str[] tags: ["production", "web", "critical"]

Arrays can span multiple lines:

str[] servers: [
  "server1.example.com",
  "server2.example.com",
  "server3.example.com",  # Trailing comma allowed
]

Empty arrays are allowed:

int[] empty: []

Global Attributes

Global attributes are top-level key-value pairs that are accessible throughout the configuration. Struct instances can access global variables in templates using the explicit {global.field} syntax.

str timezone: UTC
str environment: production
int max_connections: 1000

Note: The word global is semi-reserved for template access to global scope. As a best practice, avoid using global as a field name in struct definitions to prevent conflicts with the global access mechanism. See String Templates for more information on global variable access in templates.

Nullable Globals

Global attributes can be marked as nullable by prefixing the type with ?. Nullable attributes can have the value null.

?str optional_setting: null
?int timeout: 30

Structs

Structs are the core building blocks of Tyco, allowing you to define custom data types with strongly-typed fields. The struct system provides four main capabilities:

Structs support advanced features including primary keys for references, nullable fields, array types, and inheritance through default value cascading.

Struct Definition

Structs define structured data types with named fields. A struct definition begins with a capitalized identifier followed by a colon.

Server:
  str hostname:
  int port:
  bool ssl:

Schema Declaration

The schema section defines the structure and types of a struct's fields. Each field declaration consists of a type, field name, and optional default value or modifiers.

Field Syntax

[modifier]type[[]] field_name: [default_value]

Primary Keys

Fields marked with an asterisk (*) are primary keys, used for object lookups and references.

User:
 *str username:  # Primary key
  str email:
  int age:

Nullable Fields

Fields marked with a question mark (?) are nullable and can have the value null.

Person:
  str name:
 ?str nickname:  # Optional field
 ?int age:

Array Fields

Fields with the [] suffix are arrays of the specified type.

Application:
  str name:
  str[] tags:  # Array of strings
  int[] ports:

Struct-Typed Fields

Fields can reference other struct types.

Database:
 *str name:
  str host:
  int port:

Application:
  str name:
  Database database:  # References a Database instance

Enum Constraints

Scalar fields can restrict their allowed values by supplying a parenthesized list of choices in place of a default. Each choice is parsed using the field's declared type, and every instance must provide a value from that list.

Service:
  str name:
  str tier: (primary, backup, canary)
  int port:

Region:
  str name:
  str tier:
  - ewr1, primary
  - sjc1, backup

Enum constraints are validated before templating or object creation, so misspelled or unsupported values fail fast. Choices can span multiple lines, include quoted literals, and use any base scalar type (strings, ints, decimals, booleans, etc.). Because enums represent a constraint—not a default—Tyco requires each instance to explicitly set the field.

Default Values

Default values can be specified in the schema declaration or as separate attribute assignments.

Inline Defaults

Server:
  str hostname:
  int port: 80  # Default port
  bool ssl: false  # Default SSL setting

Separate Default Declarations

Defaults can also be set after the schema using indented attribute assignments.

Server:
  str hostname:
  int port:
  bool ssl:
  port: 80  # Default port value
  ssl: false

Multiple Struct Blocks with Different Defaults

You can create multiple struct blocks for the same type with different default values. Each block can define its own defaults and instances. This is useful for organizing instances by environment, region, or other categories.

Defaults are applied procedurally in the order they appear in the file. Each new defaults declaration updates the current default values for subsequent instances, allowing you to change defaults as you progress through the configuration.

# First Server block with development defaults
Server:
 *str hostname:
  int port:
  bool ssl:
  port: 8080  # Dev default
  ssl: false
  - dev1.internal
  - dev2.internal

# Second Server block with production defaults
Server:
  port: 443  # Production default
  ssl: true
  - prod1.example.com
  - prod2.example.com

# Third Server block with custom defaults
Server:
  port: 3000  # API default
  ssl: true
  - api1.example.com
  - api2.example.com

All instances from all blocks are collected together into a single array for the Server type.

Struct Instances

Struct instances are created using a dash (-) followed by field values. Values can be provided in order or as named assignments. Trailing commas are allowed in instance declarations.

Positional Values

When using positional values, they must match the order of struct's fields.

User:
 *str username:
  str email:
  int age:
  - alice, alice@example.com, 30
  - bob, bob@example.com, 25,  # Trailing comma allowed

Named Values

Values can be explicitly named using field_name: value syntax. These can be ommitted if using defaults, and for clarity should follow the order of field names in the schema, but technically can be in any order.

User:
 *str username:
  str email:
  int age:
  - username: alice, email: alice@example.com, age: 30
  - username: bob, email: bob@example.com, age: 25

Mixed Syntax

Positional and named values can be mixed, with positional values coming first.

User:
 *str username:
  str email:
  int age:
  - alice, email: alice@example.com, age: 30

Line Continuation

Long instance declarations can be split across multiple lines using a backslash (\) at the end of a line.

Server:
 *str hostname:
  int port:
  bool ssl:
  str description:
  - hostname: server1.example.com, port: 443, ssl: true, \
    description: "Production web server"

Object References

Objects with primary keys can be referenced by other objects. References use the syntax TypeName(primary_key_value).

Single-Key References

Database:
 *str name:
  str host:
  int port:
  - main_db, db.example.com, 3306
  - cache_db, redis.example.com, 6379

Application:
  str name:
  Database database:
  - web_app, Database(main_db)  # References Database with name "main_db"
  - cache_service, Database(cache_db)

Inline Object Creation

Objects can be created inline by providing all required field values in the reference syntax. Trailing commas are allowed in inline object creation.

Person:
  str name:
  int age:

Server:
  str hostname:
  Person owner:
  - server1, Person(Alice, 30,)  # Creates Person inline, trailing comma allowed

String Templates

String values support template interpolation using curly braces {field}. Templates are resolved after all instances are created.

Field References

Server:
 *str name:
  str domain:
  str url:
  - web1, example.com, "https://{name}.{domain}"  # Resolves to "https://web1.example.com"

Nested Field Access

Templates can access fields of referenced objects using dot notation.

Host:
 *str name:
  str ip:
  - server1, 192.168.1.10

Application:
  str name:
  Host host:
  str connection:
  - web_app, Host(server1), "{name} @ {host.ip}"  # Resolves to "web_app @ 192.168.1.10"

Field Names Containing Dots

Schema field names can legally contain dots, but it is best to avoid them to prevent confusion. Template resolution first treats each dot as a scope separator; if that lookup fails, Tyco falls back to matching the longest remaining segment as a literal field name.

Service:
 *str name:
  Host host:
  str hostname.value:
  str description:
  - edge-api, Host(edge), "literal", "host={host.name} literal={hostname.value}"

In the example above {host.name} behaves like standard dot access, while {hostname.value} resolves to the field whose literal name contains a dot.

Parent References

Templates can reference parent object fields using explicit .. prefix. Each additional dot traverses one level up in the parent hierarchy. This provides explicit control over scope traversal.

Project:
 *str name:
  str status:
  Task task:
  - WebApp, active, Task("Setup for {..name}")

Task:
  str description:

The {..name} reference explicitly accesses the parent Project's name field, making scope relationships clear and predictable.

Global Variable Access

Struct instances must use explicit global. syntax to access global variables, ensuring clarity and preventing naming conflicts. Global variables can reference other globals directly.

str company: "TechCorp"
str region: "us-west"

Person:
 *str name:
  str company:
  str email:
  - Alice, "{global.company}", "{name}@{global.company}.com"

# Global variables access other globals directly
str server_name: "{company}-api-{region}"  # Resolves to "TechCorp-api-us-west"

The explicit global. prefix makes it clear when struct instances are accessing global scope, preventing confusion with local field names. If a struct has a field named global, that field will take precedence over the global scope access mechanism. Global variables themselves can reference other globals directly without the prefix.

str company: "TechCorp"

Department:
 *str name:
  str global:
  str summary:
  - LocalScope, local-value, "{global}"  # Resolves to "local-value"

In this case {global} binds to the local field, not the global scope - however avoid naming field names global to reduce ambiguity.

Include Directive

The #include directive allows splitting configuration across multiple files.

#include shared_config.tyco
#include databases.tyco

Application:
  str name:
  Database db:
  - main_app, Database(primary)

Note: Included files must not redefine struct types already defined in the including file. Implementations may process includes recursively.

Directory Loading

Modern Tyco parsers support loading entire directory structures, automatically discovering and processing all .tyco files recursively.

Loading Directories

When a directory path is provided to the loader, all .tyco files within that directory and subdirectories are processed as a single configuration context:

# Python example
from tyco import load

# Load all .tyco files in config/ directory recursively
context = load('config/')
data = context.as_json()

Directory structure example:

config/
├── globals.tyco          # Global settings
├── databases.tyco        # Database definitions
├── environments/
│   ├── production.tyco   # Production config
│   └── staging.tyco      # Staging config
└── services/
    ├── web.tyco          # Web service config
    └── api.tyco          # API service config

Benefits of Directory Loading

Note: All files are processed in the same context, so struct definitions and references work across file boundaries. However, duplicate struct type definitions will cause errors.

Validation

Some Tyco parser implementations support language-specific validation modules that provide custom validation logic and constructors for configuration objects.

Python Validation Modules

The Python parser automatically loads corresponding .py files when processing .tyco files. If config.tyco exists alongside config.py, the Python module is imported and can provide custom validation:

Import errors (including syntax errors) in a sibling .py module now halt loading so broken validators surface immediately.

# config.tyco
User:
 *str username:
  str email:
  int age:
  - alice, alice@example.com, 30
  - bob, invalid-email, 25
# config.py
import re
from tyco.parser import Struct

class User(Struct):
    def validate(self):
        # Custom email validation
        email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(email_pattern, self.email):
            raise ValueError(f"Invalid email format: {self.email}")
        
        # Age validation
        if self.age < 0:
            raise ValueError(f"Age must be positive: {self.age}")

Validation Features

Error Handling

Validation errors are reported with clear messages indicating which configuration objects failed validation:

ValueError: Invalid email format: invalid-email
  in User(bob) at config.tyco:5

Note: Validation modules are optional. If a .py file cannot be loaded or contains errors, configuration parsing continues without validation. This ensures that malformed validation code doesn't break configuration loading.

ABNF Grammar

This section provides a formal grammar for Tyco in Augmented Backus-Naur Form (ABNF) per RFC 5234.

; Document Structure
tyco-document = *( global-attr / struct-def / include / comment / newline )

; Global Attributes
global-attr = [ nullable ] type-name [ array-marker ] ws+ identifier ws* ":" ws* value newline

; Struct Definition
struct-def = struct-header schema-section [ defaults-section ] [ instances-section ]
struct-header = identifier ":" newline
schema-section = 1*( ws+ [ modifier ] type-name [ array-marker ] ws+ identifier ws* ":" [ ws* value ] newline )

; Modifiers
modifier = primary-key / nullable
primary-key = "*"
nullable = "?"
array-marker = "[]"

; Default Values
defaults-section = *( ws+ identifier ws* ":" ws* value newline )

; Instances
instances-section = *( ws+ "-" ws* instance-values [ "\" newline ] newline )
instance-values = value *( ws* "," ws* [ identifier ws* ":" ws* ] value )

; Values
value = string / integer / float / boolean / null / array / reference
string = basic-string / multiline-basic / literal-string / multiline-literal
basic-string = %x22 *( string-char / escape-seq ) %x22
multiline-basic = %x22.22.22 *( string-char / newline / escape-seq / line-ending-backslash ) %x22.22.22
literal-string = %x27 *literal-char %x27
multiline-literal = %x27.27.27 *( literal-char / newline ) %x27.27.27

; Escape Sequences
escape-seq = "\\" ( %x62 / %x74 / %x6E / %x66 / %x72 / %x22 / %x5C / unicode-escape )
unicode-escape = "u" 4HEXDIG / "U" 8HEXDIG
line-ending-backslash = "\\" ws* newline ws*

; Numbers
integer = [ "+" / "-" ] ( decimal / hexadecimal / octal / binary )
decimal = 1*DIGIT
hexadecimal = "0x" 1*HEXDIG
octal = "0o" 1*( %x30-37 )
binary = "0b" 1*BIT
float = [ "+" / "-" ] 1*DIGIT "." 1*DIGIT

; Boolean and Null
boolean = "true" / "false"
null = "null"

; Array
array = "[" [ ws* value *( ws* "," ws* value ) [ ws* "," ] ] ws* "]"

; Reference
reference = identifier "(" ( identifier / ( value *( "," ws* value ) ) ) ")"

; Types
type-name = "str" / "int" / "float" / "bool" / "date" / "time" / "datetime" / identifier

; Identifiers
identifier = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" / "-" )

; Comments and Includes
comment = "#" *( %x09 / %x20-10FFFF ) newline
include = "#include" ws+ path newline
path = 1*( ALPHA / DIGIT / "." / "/" / "_" / "-" )

; Whitespace
ws = %x20 / %x09  ; Space or Tab
newline = %x0A / %x0D.0A  ; LF or CRLF

; Character Classes
string-char = %x20-21 / %x23-5B / %x5D-10FFFF  ; Any Unicode except " and \
literal-char = %x20-26 / %x28-10FFFF  ; Any Unicode except '