Tya Language Specification

Status: current repository specification. This page describes the language surface maintained on main, including the current package, tooling, concurrency, interface, and standard-library integration rules.

Overview

Tya is an indentation-based, dynamically typed language that compiles to C. The implementation is intentionally small and explicit: source is tokenized, parsed into an AST, checked, emitted as C, and linked with the Tya runtime.

Tya’s user-facing commitments are:

This document specifies the language, built-in function surface, standard-library surface, package rules, and tool surface.

v1.0.0 Compatibility Boundary

Tya v1.x compatibility covers accepted syntax, documented runtime behavior, documented public standard-library and package APIs, stable diagnostic codes, the CLI JSON diagnostic schema, and release artifact semantics described in this specification. Undocumented implementation details, generated C internals, internal Go package layout, unsupported experimental flags, external package internals, and post-v1 ecosystem packages are not compatibility guarantees.

The self-hosted compiler is the primary compiler direction for v1.0.0. The Go implementation remains in the repository as a reference implementation and bootstrap recovery path until the no-Go self-host bootstrap proof replaces that recovery role.

Specification authority for v1.0.0 is, in order: this specification and the frozen docs/v1.0/SPEC.md; the latest self-host compiler behavior when it implements the documented v1 surface; then the Go implementation as reference and bootstrap recovery path. If the Go implementation and latest self-host compiler disagree, the behavior matching the v1 specification is authoritative.

The strict-semantics rule matrix in docs/STRICT_SEMANTICS.md is normative for v1.0.0 validity boundaries and records the active parser, checker, runtime, CLI, LSP, and self-host coverage for each rule family.

Completed feature specs under feature-specs/completed/ are design history. They may explain why a rule was accepted, but users do not need them to know the current v1.0.0 contract. Public v1 authority is this specification, docs/STRICT_SEMANTICS.md, and the frozen documents under docs/v1.0/.

Source and Lexical Structure

Examples use ordinary Tya source. Grammar fragments are illustrative rather than a complete parser grammar.

snake_case            variable, function, method, import path segment
SCREAMING_SNAKE_CASE  constant
PascalCase            class and interface

The words “must”, “must not”, “may”, and “should” are normative when they describe program validity or implementation behavior.

Naming

Tya names express naming category, not accessibility. Accessibility is expressed by language constructs such as private.

Value names, function names, method names, file names, import path segments, and dictionary keys use snake_case. Constants use SCREAMING_SNAKE_CASE. Classes and interfaces use PascalCase. Callable names may add exactly one trailing ? predicate suffix or exactly one trailing ! bang suffix. These suffixes are only valid for function and method declarations and calls; they are not valid for variables, constants, fields, import aliases, file names, or import path segments.

Single-file imports use the source filename without .tya as the import path segment. Import paths are slash-separated snake_case segments. Leading _ has no visibility meaning for ordinary bindings. Standard-library APIs use snake_case; CamelCase builtin spellings are not part of the language surface.

Source Code Representation

Tya source is UTF-8 text. The compiler normalizes CRLF line endings to LF before lexing. Source files use .tya.

Indentation defines blocks. Spaces are the indentation unit. Tabs are forbidden in source indentation and in heredoc body indentation.

if ready
  print("ready")
else
  print("not ready")

Each physical line is part of one logical line except when it is inside a parenthesized call, an array literal, a string literal, or a canonical continuation form accepted by the parser and formatter.

Lexical Elements

Comments

Line comments begin with # and continue to the end of the line.

# file header comment
name = "tya" # line-end comment

Comments may attach to declarations and statements for formatting, LSP hover, and tya doc. Comment placement rules are part of Formatted Syntax. Declarations include top-level class, interface, struct, and record declarations and their body members.

Tya recognizes three source comment roles:

Comments in positions with no definite attachment target are invalid. A block whose body contains only comments is invalid because it has no executable or declarative body item.

Tokens

The token vocabulary includes identifiers, literals, indentation tokens, operators, and punctuation.

= ?? ??= == != < <= > >= : , . ? ! @ + - * / % ->
( ) [ ] { }
& | ^ ~ << >>

Whitespace separates tokens. Newlines are significant because they terminate statements and define indentation blocks.

Identifiers

Identifiers are ASCII-oriented by convention and by the current naming rules. Public variable, function, method, file, and import path names use snake_case. Function and method names may end with one ? or one !; ? and ! suffixes are mutually exclusive and may not appear in the middle of a name. Class and interface names use PascalCase. Constants use SCREAMING_SNAKE_CASE.

The following words are reserved in positions where ordinary names are parsed:

abstract and as await break case catch class continue default else elseif embed
extends false final for if implements import in interface module nil not or
override private raise return scope select self Self spawn static super true try
while with

Some words are context-sensitive. For example, as is meaningful in imports, extends and implements are meaningful in class and interface headers, and case, default, send, receive, and timeout are meaningful inside select.

Literals

Tya has literals for nil, booleans, numbers, strings, bytes, arrays, and dictionaries.

missing = nil
ready = true
count = 42
ratio = 3.14
name = "Tya"
data = b"abc"
items = [1, 2, 3]
user = { name: "komagata", age: 20 }

String literals use double quotes. Strings support interpolation with {...}.

print("Hello, {user["name"]}")

Triple-quoted strings and heredoc forms are available for multi-line text. Raw and byte heredoc forms preserve their documented escaping behavior. The formatter treats multi-line strings as atomic except where Formatted Syntax defines a rewrite.

Byte literals use b"..." or byte heredoc forms and produce byte values rather than strings.

Integer literals may be written in decimal, hexadecimal, or binary form. Floating-point literals use decimal notation. NaN, Infinity, nan, and infinity are ordinary identifiers when used as names; they are not numeric literal spellings.

Tya String values are UTF-8 text. Text file APIs that return strings reject invalid UTF-8. Binary file APIs return Bytes and do not validate UTF-8.

Values And Kinds

Tya is dynamically typed. Values carry a runtime kind. The core runtime kinds are:

nil
bool
number
string
bytes
array
dict
function
class
object
error
task
channel
resource

Arrays and dictionaries are mutable. Strings and bytes are separate value kinds. Classes are runtime values; object values are instances of classes.

Primitive values expose methods through runtime wrapper classes and standard builtins.

print(" tya ".trim().upper())
print([1, 2, 3].len())
print({ name: "tya" }.keys())
print(value.class)

Tya does not perform implicit conversions. Operations that require a number, string, array, dictionary, function, class, task, channel, or resource must receive a value of the required kind or raise a runtime error. The documented exceptions are formatting operations such as string interpolation and to_string(), plus the exact operator cases listed below.

Blocks

A block is a non-empty sequence of statements introduced by a header line and an increased indentation level. Empty blocks are invalid; use an explicit expression such as nil for an intentional no-op body.

while count < 3
  print(count)
  count = count + 1

Bindings created inside if, while, for, catch, match case, scope, and select bodies are local to that body. Assigning to an existing outer non-function binding from such a nested block updates the outer binding.

Blocks appear in control-flow statements, function bodies, class bodies, interface bodies, try / catch, scope, select, and similar constructs.

Top-level source consists of imports, declarations, assignments, and statements allowed by the file kind. Class files are more restrictive than script files.

File Kinds

A .tya file’s role is determined by its filename and context.

snake_case .tya files are script files unless their contents satisfy the class/interface/struct/record file rules. Script files may be entry files for tya run and may also be imported directly. When imported, their top-level names are exposed through the import binding.

Class/interface/struct/record files use snake_case filenames and are library-only; they cannot be entry files. Such a file must declare exactly one public class, interface, struct, or record whose PascalCase name maps to the filename without .tya, such as base64.tya declaring class Base64 or http_server.tya declaring class HTTPServer. PascalCase filenames such as Base64.tya are not class files.

Class files may be loaded explicitly as part of a directory package or implicitly as same-directory siblings of an entry script. A script entry sees snake_case type files in its own directory without import.

This filename convention is a minor-version language/package convention change from the older PascalCase class-file convention. Existing Base64.tya-style files must be renamed to the corresponding snake_case.tya form.

Accepted Syntax and Formatted Syntax

Tya has a two-layer source model. Accepted Syntax is the source surface accepted by the lexer and parser. Formatted Syntax is the deterministic representation emitted by tya format. Every accepted program has exactly one standard formatted representation, but accepted source may have editing-friendly spelling that the formatter rewrites.

tya check, tya run, tya build, tya format, tya lint, and LSP read the same Accepted Syntax. Documentation, examples, stdlib, and self-host sources should stay in Formatted Syntax.

Formatted Syntax covers indentation, blank lines, comment attachment, line wrapping, import grouping, operator spacing, string literal forms, empty collection forms, and other source-shape decisions. The formatter is the formatted syntax serializer and has no style configuration.

The core formatted rules are:

Implementations must preserve semantic behavior when formatting. Formatting must be idempotent and stable across platforms.

V1 Language Boundaries

Tya v1.0.0 intentionally keeps the syntax surface small. The following forms are not part of v1.0.0 and must fail before code generation:

Declarations And Scope

Bindings

Assignment creates or updates bindings.

name = "Tya"
count = count + 1

Reassignment must preserve the binding’s runtime kind, except that nil may move to or from a concrete kind because it represents absence. Assigning nil does not erase the last known concrete kind. A name first assigned a number may later receive another number or nil, but not a string, array, dictionary, function, class, object, error, or resource value. This keeps Tya dynamically typed while making rebinding strict and predictable.

count = 1
count = 2      # valid
count = "two"  # invalid

err = nil
err = error("failed") # valid

Multiple assignment is supported.

min, max = bounds(items)

Leading _ has no visibility meaning for ordinary bindings. Top-level privacy is not expressed by name spelling.

Constants use SCREAMING_SNAKE_CASE and are checked as constants by naming and assignment rules. Constants cannot be reassigned. Heap-backed values stored in constants are also immutable through that constant binding.

Class member privacy uses the private keyword for private class fields, class constants, methods, class variables, class methods, and constructors. Protected visibility uses the protected keyword for instance and static methods that are callable from the declaring class and descendant classes, but not from unrelated code or same-package peers. Protected visibility does not apply to fields, constants, class variables, or constructors.

class User
  private ROLE: "user"

  private id: 0

  private normalize: ->
    Self.ROLE + ":" + self.id.to_string()

  protected label: value ->
    "user:{value}"

Embedded Assets

embed declares a top-level binding whose value is loaded from a file at build time. Embed declarations are resolved relative to the source file.

embed "templates/index.html" as index_html

Embed transforms are implementation-defined by the compiler surface and must produce ordinary Tya values.

Functions

Functions are values. Function literals use ->.

greet = name -> "Hello, {name}"

double = value ->
  result = value * 2
  result

Formatted Syntax prefers the one-line form for a single-expression body:

answer = -> 42

A block containing only return value also formats to -> value. Multi-statement bodies, bodies with attached comments, and one-line renderings that exceed the column limit stay block-bodied.

Calls always use parentheses.

print(greet("Tya"))

Calls may pass arguments by parameter name. Keyword names bind to the existing parameter names; there is no separate keyword-parameter declaration syntax. Positional arguments may appear before keyword arguments, but not after them.

request = url, timeout = 30, method = "GET" ->
  [url, timeout, method]

request("https://example.test", timeout: 10)
request(method: "POST", url: "https://example.test")

Calls may also use an indented keyword argument block. This is a multi-line keyword call, not a dictionary literal.

render
  title: "Home"
  user: "komagata"

Required parameters may be supplied by keyword. Unknown keywords, duplicate keywords, and parameters supplied both positionally and by keyword are invalid. Dictionary expansion with **expr expands string keys into keyword arguments.

options = { timeout: 10, method: "GET" }
request("https://example.test", **options)

Array splat calls such as fn(*items) remain invalid; pass arrays explicitly.

The final evaluated statement or expression in a function body is returned implicitly when no explicit return exits first. Use return for early return or multiple return values.

parse_user = text ->
  if text == ""
    raise error("empty user")
  { name: text }

Parameters are local bindings. _ may be used for intentionally ignored parameters.

Classes

A class declares a runtime class value.

class User
  name: ""

  initialize: user_name ->
    self.name = user_name

  label: ->
    "user:{self.name}"

Instances are constructed by calling the class.

user = User("komagata")
print(user.label())

initialize is the constructor hook. Constructor calls use the same default-argument rules as ordinary method calls: callers may omit trailing defaulted initialize parameters, while required constructor parameters and the total parameter count are still enforced. Instance methods receive self. Instance fields are declared in the class body and are assigned with self.<name> = value; assigning an undeclared self.<name> is invalid, including inside initialize. Method parameters and local bindings may reuse instance field names; the bare name refers to the local binding or parameter, and the field remains available through explicit self.<name> access.

An object that provides an instance method named call is callable with function-call syntax. handler(args) is equivalent to handler.call(args) for such objects, including keyword arguments and dynamically produced receiver expressions. A field named call is not enough; the member must be an instance method. Calling a known class instance that has no visible instance call method is a checker error, and dynamic non-callable object calls fail at runtime. Class objects are not function objects: Foo(args) remains a constructor call even when Foo defines static call, while Foo.call(args) remains the explicit static method call.

Inside initialize and instance methods, an unqualified call such as helper(args) resolves to self.helper(args) when ordinary callable lookup does not find helper and the current class, a parent class, or an interface default provides that instance method. If no instance method matches, the same unqualified call resolves to Self.helper(args) when the current class or a parent class provides that static method. Static methods also resolve helper(args) to Self.helper(args) for static methods on the current class or parent class. Inside initialize and instance methods, an unqualified field read such as items resolves to self.items when ordinary lexical lookup does not find items and the current class, a parent class, or an implemented interface declares that instance field. This bare field read may be used as a member target, such as items.push(value). Field writes remain explicit: items = value assigns a local binding, while self.items = value assigns the instance field. Bare references such as value = helper are not receiver method references. Formatted Syntax prefers the unqualified call form for same-class receiver method calls: self.helper(args), Self.helper(args), and CurrentClass.helper(args) are formatted as helper(args) when they appear as calls inside CurrentClass. Same-class class constant reads such as Self.NAME and CurrentClass.NAME are formatted as NAME. Same-class self.field reads are formatted as field when no method parameter or local binding with that name can change the meaning. Formatted Syntax writes callable-object calls directly: handler.call(args) is formatted as handler(args) when the receiver expression is not a class object. Explicit static calls such as Foo.call(args) and pkg.Foo.call(args) are preserved.

Tya supports:

Class constants are class-owned immutable members. Inside the defining class, canonical access is the bare constant name, such as NAME. Public class constants may be read as pkg.Class.NAME; private class constants may only be read from the defining class. static NAME: ... is a class variable spelling and is not the canonical constant form.

class Admin extends User
  initialize: admin_name ->
    super(admin_name)

  override label: ->
    "admin:{self.name}"

Subclasses may override a protected method as protected or public. A subclass may not reduce visibility by overriding a protected method as private or a public method as protected/private.

A type file is a snake_case .tya file. It must declare exactly one public class, interface, struct, or record whose PascalCase name maps to the filename. It may also declare private helper types. Type files are library files and cannot be run as entry scripts.

Additional types in a type file are private to that file. They are not visible from other files, even inside the same directory package.

Structs And Records

struct and record declare lightweight named-field data types.

struct User
  name
  age: 0

record Point
  x
  y

Fields without defaults are required constructor parameters. Fields with : defaults may be omitted. Required fields must precede defaulted fields, and field order defines positional constructor order. Constructors use ordinary call binding, including keyword arguments:

user1 = User("komagata", 45)
user2 = User(name: "komagata", age: 45)
user3 = User("komagata")

Struct fields are mutable. Record fields are read-only after construction. Both reject assignments to undeclared fields. Structs and records compare by declared type and field values; two different declarations are unequal even when their fields match.

Records provide with(...), which accepts only keyword arguments or **dictionary keyword expansion and returns a new record value with selected fields replaced. Structs do not provide with(...).

Struct and record bodies may contain only field declarations. They do not support methods, static members, private/protected members, inheritance, interfaces, class variables, class constants, or runtime type generation. Leading comments may attach to struct and record declarations and to their field declarations, using the same placement rules as class declarations and class fields.

Structs and records may be the public type in snake_case type files and are exported by directory packages under the same public-name rules as classes and interfaces.

Interfaces

Interfaces are explicit contracts and stackable behavior units.

interface Named
  name: ->

  label: ->
    name()

An interface may contain:

An interface may not contain static members, private members, nested classes, or nested interfaces. Self is invalid inside interface methods.

Classes list implemented interfaces with implements.

interface Timestamped
  created_at: nil

  initialize: ->
    self.created_at = Time().now()

class Account implements Named, Timestamped
  name_value: ""

  initialize: account_name ->
    self.name_value = account_name
    super()

  name: ->
    self.name_value

Default methods are inherited when the class does not define a method with the same name. A class method wins over an interface default. Interface defaults stack in the declared implements order and may call super().

Interface fields contribute instance state. A class that implements multiple interfaces must not receive conflicting field definitions. If a class constructor implements interfaces with initializer hooks, it must call super() exactly where it wants the interface initialization chain to run.

Interface conflict rules are strict:

Interfaces declared in class files are exported as package public names unless their names begin with _.

Comparable is the standard ordering protocol. A class implements it by providing compare(other), which returns a negative number, zero, or a positive number when the receiver sorts before, equal to, or after other. Numbers conform to Comparable as primitive values. Strings do not support the primitive ordering operators; locale-aware or lexicographic collation must use explicit methods or standard-library APIs. The ordering operators <, <=, >, and >= keep their primitive behavior and do not dispatch to user-defined compare.

Equatable is the standard domain equality protocol. A class implements it by providing equal?(other), which must return a boolean. Primitive values expose equal?; scalar primitives follow ==, while arrays and dictionaries use deep equality. The == operator and top-level equal(left, right) keep their existing behavior and do not dispatch to user-defined equal?.

Stringable is the standard human-readable formatting protocol. A class implements it by providing to_string(), which returns a string and should be side-effect free for ordinary formatting use. Number, String, Array, Dict, Boolean, and Nil conform to Stringable as primitive values without changing their tagged runtime representation or value.class behavior. Stringable is not a structured serialization protocol; use Serializable.to_data() for data trees.

Debug display uses inspect(). inspect(value) and value.inspect() return a stable developer-facing string. Strings are quoted and escaped, arrays and dictionaries inspect nested values, and objects without a custom inspect() show their class name and public fields. Struct and record values use TypeName(field: value, ...) for both normal display and inspect.

The standard library also defines protocol interfaces for iteration, sequences, I/O, and structured data:

Arrays, dictionaries, strings, and bytes conform to Iterable as primitive values. String iteration yields one-character strings. Bytes iteration yields numeric byte values. for ... in consumes primitive iterables directly and consumes user-defined iterables through iter(). I/O protocol interfaces are defined in the relevant standard-library packages such as io and net/socket; they document shared stream behavior and are implemented by concrete reader, writer, socket, and server classes where their methods match.

Expressions

Expressions compute values.

Primary Expressions

Primary expressions include identifiers, literals, parenthesized expressions, function literals, indexing, member access, calls, self, Self, and super.

user["name"]
items[0]
user.label()
User("komagata")
self.name
super(name)

self is available inside instance methods and constructors. Self refers to the current class in class contexts where it is valid. super(...) delegates to the parent constructor, parent method, or next stacked interface method depending on context.

Function literals are lexical closures. A function literal may read parameters and local bindings from enclosing function bodies. Captures are value snapshots created when the function literal is evaluated; heap-backed values such as arrays, dictionaries, objects, functions, resources, and tasks are captured as values, not deep-copied. Top-level names are not captured and continue to use module/global lookup.

Function bodies cannot write back to outer function bindings. Direct reassignment of an outer function binding is invalid, and indexed or member assignment through a captured outer function binding is invalid. Pass mutable state as an explicit parameter when a function is intended to mutate that value.

Each evaluation of a function literal creates an independent closure environment.

Function literals themselves have no declaration name. When a function value is assigned to a binding, the binding may be used as a debugging/display name; that name does not affect equality, identity, or call behavior.

Function parameters may have default values. Required parameters must precede defaulted parameters, calls may omit only a trailing run of defaulted parameters, and too few required arguments or too many arguments are invalid. Default expressions are evaluated at call time, left to right, and may reference earlier parameters but not later parameters. Mutable defaults are fresh because the default expression is re-evaluated for each omitted argument. Variadic parameter syntax is not part of Tya; pass an array explicitly when a function needs a variable number of values.

make_adder = base ->
  value -> base + value

add_two = make_adder(2)
add_ten = make_adder(10)

print(add_two(3))
print(add_ten(3))

Reassigning the original local after a closure is created does not change the captured value.

make_label = name ->
  label = -> name
  name = "changed"
  label

print(make_label("first")())

Mutating through a captured binding is invalid. The closure must receive the mutable value as a parameter if mutation is intended.

make_bad = items ->
  ->
    items[0] = "changed" # invalid: cannot mutate captured binding

Operators

Tya supports arithmetic, comparison, logical, and bitwise operators.

??
or
and
not
== != < <= > >=
| ^ &
<< >>
+ -
* / %

Logical operators use words: and, or, and not.

if ready and not disabled
  print("ok")

The nil-coalescing operator ?? evaluates and returns its left operand when it is not nil; otherwise it evaluates and returns its right operand. Only nil falls through. false, 0, 0.0, "", empty arrays, and empty dictionaries are preserved.

name = maybe_name ?? "anonymous"

Arithmetic operations require numbers unless a documented primitive method or operator case says otherwise. + adds two numbers, concatenates two strings, and concatenates two bytes values. + does not format mixed operands through implicit string conversion. String interpolation formats embedded values with the display surface. / performs integer division when both operands are integer-compatible numbers and number division when either operand is a floating-point value, so 5 / 2 evaluates to 2 and 5.0 / 2 evaluates to 2.5. Integer division truncates toward zero, so -5 / 2 evaluates to -2. % is integer-only, follows the same truncation model, and is invalid for floating-point operands. nil arithmetic is invalid.

and and or return booleans. They test operands with Tya truthiness, do not return either operand as a value, and short-circuit: and skips the right operand when the left operand is falsey, while or skips the right operand when the left operand is truthy.

Method-call receivers are evaluated exactly once before method lookup and argument evaluation continues.

Bitwise operators require integer-compatible number values.

Equality operators may compare any two runtime values without coercion. Values with different runtime kinds compare unequal. Arrays and dictionaries compare by contents; functions, classes, objects, resources, tasks, and channels compare by identity unless their documented primitive surface says otherwise. Numeric int and float values compare as one number kind, so 1 == 1.0 is true. Ordering operators <, <=, >, and >= require numbers.

Deep equality on cyclic arrays or dictionaries is a runtime error. Display of cyclic arrays or dictionaries must terminate with a stable cycle marker.

Collections

Arrays use bracket literals and integer indexing.

items = ["a", "b"]
items.push("c")
print(items[0])

Array assignment blocks are accepted when = is followed by an indented list of element expressions. The block is equivalent to a bracket array literal. Use [] for an empty array.

items =
  "a"
  "b"
  "c"

Dictionaries use brace literals. Identifier keys and string-literal keys in dictionary literals are stored as string keys. String-literal keys support JSON-style names such as "Content-Type", "$schema", "1", and "". Duplicate keys are invalid after normalizing identifier and string-literal forms to strings.

user = { name: "komagata", age: 20 }
print(user["name"])
user["admin"] = true

Dictionary assignment blocks may contain nested dictionary block values.

user =
  name: "komagata"
  profile:
    github: "komagata"

Function and method bodies may return a dictionary by ending with a bare dictionary block.

summary = ->
  name: "komagata"
  active: true

Array and dictionary block forms and empty collection forms are canonicalized by the formatter. Assigned arrays format in bracket style when they fit within 80 characters, and in assignment block style when bracket style would exceed 80 characters. Dictionary expressions format in brace style when they fit within 80 characters, and in supported block style when brace style would exceed 80 characters.

Brace-less dictionary literals are not accepted as array elements or call arguments. Use braces for dictionary values in those positions.

Dictionary keys are read and written with string indexes. Dot access on dictionaries is reserved for documented dictionary receiver methods such as keys(), has?(), get(), set(), and delete(); dictionary key member access such as user.name is invalid.

Array, string, and bytes indexes must be integers. Dictionary and error-value indexes must be strings. Missing dictionary keys and out-of-range array, string, or bytes indexes return nil; indexing a non-collection target is invalid. Negative indexes are invalid for arrays, strings, and bytes. Slice syntax such as items[1:3] is not part of the language; use explicit methods such as items.slice(1, 3). String indexing is by Unicode rune. Bytes indexing is byte-based.

Array index assignment requires an existing array index and fails on out-of-range writes. Dictionary index assignment may create a new key. Arrays and dictionaries are mutable. Strings and bytes are immutable. Array.push, Dict.set, Dict.merge!, and Dict.delete return nil. Array.pop returns the removed value, or nil when the array is empty.

Error Values

error(message, options = {}) creates an error value. message, kind, and code are strings; data is a dictionary; and cause is another error value or nil. Unknown option keys are invalid. Error display uses message.

err = error("not found", {
  kind: "io",
  code: "file_not_found",
  data: { path: "missing.txt" }
})
print(err["message"])
print(err["kind"])
print(err["code"])

Concurrency Expressions

spawn starts a task and returns a task value. await waits for a task and returns or re-raises its result.

task = spawn work(21)
print(await task)

Channels and sync resources are standard-library-backed runtime values with the methods specified in the Standard Library section.

Parallelism And Concurrency

Tya exposes structured concurrency through tasks, scopes, channels, sync resources, and select.

Tasks are lightweight runtime values created by spawn. await joins a task. Awaiting a completed task returns the cached result or re-raises the cached error.

scope defines a structured lifetime for tasks spawned inside it. A scope waits for its child tasks before leaving the region.

Channels and sync resources are implemented by the runtime and surfaced through standard-library classes and methods. select waits across channel send, receive, timeout, and default branches.

The runtime may run tasks in parallel where the target platform and runtime support it. Program correctness must not depend on a specific scheduling order except where the language or standard library documents an ordering guarantee.

Statements

Expression Statements

Calls and other useful expressions may appear as statements.

print("hello")
save_user(user)

Assignment Statements

Assignment updates a binding, field, or indexed collection entry.

name = "Tya"
self.name = name
items[0] = "first"
user["admin"] = true

Multiple assignment evaluates the right-hand side and binds the corresponding left-hand targets. Right-hand expressions are evaluated first, left to right; after that, assignment targets are evaluated and assigned left to right.

Nil-coalescing assignment uses ??= and is available for the same single assignment targets as =. It reads the target’s current value; if that value is nil, it evaluates the right-hand side and assigns it. If the current value is not nil, the right-hand side is not evaluated and no assignment happens. Only nil triggers assignment: unlike Ruby’s ||=, false, 0, 0.0, "", empty arrays, and empty dictionaries keep their existing value.

value = nil
value ??= "fallback"

If Expressions

if, elseif, and else select among blocks and may be used as either a statement or an expression. An if expression evaluates to the final value of the selected branch body. If no branch is selected and there is no else, the value is nil.

if age >= 20
  print("adult")
elseif age >= 13
  print("teen")
else
  print("young")

elseif is the canonical spelling. else if is not canonical Tya. Only nil and false are falsey. All other values, including 0, "", empty arrays, and empty dictionaries, are truthy.

While Expressions

while repeats while its condition is truthy and may be used as either a statement or an expression.

while count < 3
  print(count)
  count = count + 1

while evaluates to the last completed body value. If the body never executes, the value is nil. break exits the nearest loop and leaves the loop value as the last completed body value before the break, or nil if there is none. continue skips to the next iteration and discards the current iteration’s partial value.

For Expressions

for ... in is the canonical way to consume iterable values. Arrays yield elements, strings yield characters, dictionaries yield { key: key, value: value } entry dictionaries, and user values participate by exposing iter(). The second binding receives a zero-based index when present. for may be used as either a statement or an expression.

for item in items
  print(item)

for item, index in items
  print("{index}: {item}")

for entry in user
  key = entry["key"]
  value = entry["value"]
  print("{key}: {value}")

for evaluates to the last completed body value. An empty loop evaluates to nil. break exits the nearest loop and leaves the loop value as the last completed body value before the break, or nil if there is none. continue skips to the next iteration and discards the current iteration’s partial value.

Match Expressions

match selects one case block by comparing an expression to case patterns and may be used as either a statement or an expression. A match expression evaluates to the selected case body’s final value. If no case matches, the value is nil. case _ is the wildcard case and is canonical only as the final case.

match value
  case "ok"
    print("ok")
  case _
    print("other")

Return Statements

return exits the current function or method. It may return zero, one, or multiple values.

return
return value
return min, max

Raise, Try, And Catch Statements

raise raises an error value. raise nil and other non-error values are invalid. try is a statement only; try expressions are not part of v1.0.0. catch err is the only catch syntax and catches raised error values. Typed catch, pattern catch, catch filters, and multiple catch clauses are invalid. Branch by error details inside the catch body with if or match. finally may follow try/catch or a bare try block. A bare try with neither catch nor finally is invalid.

finally always runs as control leaves the try or catch body, including normal completion, return, raise, break, and continue. The value of a finally body is ignored for ordinary completion. If the finally body performs its own control flow, that control flow replaces any pending control flow from the try or catch body.

try
  save_user(user)
catch err
  print("save failed: {err}")
finally
  cleanup()

Scope Statements

scope defines a structured concurrency region. Tasks spawned inside the scope are joined according to the runtime scope rules before the scope exits.

scope
  task = spawn work()
  print(await task)

Runtime Boundaries

defer, language-level assert, and language-level cancellation syntax are not part of v1.0.0. Cleanup is written with try/finally and explicit resource methods such as close(). Programs must not rely on GC finalizers for correctness.

Channels have fixed closed-state behavior: receiving from a closed channel returns nil, and sending to a closed channel raises an error. Task lifetime is structured by scope, which waits for child tasks before leaving. v1.0.0 does not define a cancellation statement or a language-level cancellation token; cancellation helpers are ordinary library APIs where documented.

Select Statements

select waits on channel operations, timeouts, and default branches.

select
case value = receive ch
  print(value)
case send ch, next
  print("sent")
timeout 1
  print("timeout")
default
  print("none")

The exact channel methods and sync primitives are defined in the Standard Library section.

Built-In Functions

Tya keeps the public builtin surface intentionally small. File, directory, path, process, stream, bytes, random, compression, digest, socket, compiler, and collection helper operations are exposed through class-style standard library APIs. Low-level runtime intrinsics may exist internally to implement those classes, but they are not public standalone builtins.

Public builtins:

print(value)
println(value)
error(message)
exit(status)
args()
env(name)

print and println write only to stdout. stderr output is available through explicit standard-library APIs such as Io().stderr().

Use standard-library APIs such as File().read(path), File().append(path, text), Dir().list(path), Path(path).expand_user(), Process.cwd(), Process.chdir(path), Io().open(path, mode), Reader#read(size), Writer#write(value), Random().int(min, max), compress.Gzip().compress(value), Digest.sha256(value), Socket.connect(host, port, options), Lexer().lex(source), Parser().parse(source), Checker().check(source), and Format().format(source) instead of low-level intrinsic names. Use receiver methods for conversions and collections, for example value.to_string(), value.to_i(), dict.delete(key), dict.keys(), and items.pop().

Standard library APIs are imported with the same import syntax as user code.

Imports and Packages

Current Tya documentation uses these terms normatively:

language feature             syntax or semantics built into Tya
built-in function            function available without import
built-in class               class available without import; none currently
user package                 importable directory of snake_case type files
user library                 reusable directory tree of user packages
standard-library package     .tya source shipped with Tya and imported normally
bundled library              library or support file shipped with the toolchain
native-backed stdlib module  importable stdlib API backed by runtime or host code
package                      versioned dependency unit declared by tya.toml
package tool                 [tools] entry run by tya tool

Language features are not imported and cannot be shadowed. Standard-library packages are specified in the Standard Library section; they are imported packages, not builtins.

Import Syntax

Imports appear at top level before other declarations and statements.

import greeting
import net/http/client
import net/http/* as http

When there are multiple imports, Formatted Syntax groups them under one import header:

import
  greeting
  net/http/client
  net/http/* as http

Import paths are slash-separated snake_case segments. Relative filesystem paths, absolute paths, empty segments, and non-snake_case segments are invalid. Package-wide imports use an explicit final /* suffix. * is only valid as the final segment in path/*; *, base64*, base64/*/foo, and base64/** are invalid.

Directory Packages

A directory package is a directory resolved by import path containing snake_case type files. It must contain at least one type file and must not contain script files at the package leaf.

Unaliased wildcard directory imports expose public class, interface, struct, and record names through the import path namespace. They do not import public names bare.

import net/http/*

server = net.http.Server()

In the example above, Server() is invalid. The first namespace segment is reserved for qualified access, so a top-level net binding in the importing file is invalid.

Aliased wildcard directory imports expose package public names through the alias namespace and do not create the original import-path namespace.

import net/http/* as http

server = http.Server()

With an alias, net.http.Server() and bare Server() are invalid in the example above unless another import creates those names.

Use as * when public names should be imported bare.

import net/http/* as *

server = Server()

Unaliased namespace imports reserve their qualified public names. Two imports that expose the same qualified public name are invalid, but net.http.Server and net.tcp.Server may coexist. Alias namespace imports reserve their alias namespace. Explicit as * imports reserve bare public names, so two as * imports that expose Server are invalid. Importing the same path twice is invalid even when aliases differ.

import net/http/*

net = "local" # invalid: net is an import namespace

An unaliased namespace import does not reserve the bare public name. A local top-level class named Server may coexist with net.http.Server.

Within the same directory package, sibling public classes are visible by bare PascalCase name without import.

The public API of a directory package is the set of public classes, interfaces, structs, and records in its snake_case type files. A type is public when its PascalCase name maps to its filename. Additional types in a type file are private to that file.

User Libraries

A user library is a directory tree of importable source intended for reuse. It does not require a manifest. A library root may be made available through TYA_PATH.

TYA_PATH=libs/web tya run app.tya

TYA_PATH entries are import roots, not relative import syntax. Source inside a user library should use the same import paths that applications use.

Resolution Order

Imports are resolved in this order:

  1. the importing file’s directory;
  2. manifest-declared dependencies from tya.lock;
  3. directories listed in TYA_PATH, from left to right;
  4. the bundled lib/ directory.

The first matching file or package directory wins. Local application imports may shadow package dependencies, TYA_PATH, and standard-library imports. Package dependencies may shadow TYA_PATH and standard-library imports.

Package Manifests

tya.toml declares package metadata, dependencies, native wrappers, and package-provided tools. Unknown top-level keys are errors so typos are not silently ignored. Package manifests require name and version; license is recommended package metadata and is preserved when present. tya install resolves dependencies and writes tya.lock. Git and explicit local path dependencies are supported. Registry-style implicit package source discovery is not supported. There is currently no central package registry and no tya publish command.

A package is a versioned distribution unit for reusable Tya code. Package code normally exposes importable source under src/. Applications consume packages through manifest dependencies:

[dependencies]
my_lib = { git = "https://github.com/example/my_lib", tag = "v0.1.0" }
local_lib = { path = "../local_lib" }

tya.lock records resolved dependency sources, revisions, and content hashes where applicable, and should be committed by applications. When tya.lock exists, it is authoritative for dependency versions and sources. If tya.toml and tya.lock disagree, commands that resolve package imports fail with a stale-lock diagnostic and instruct the user to run tya install before the changed dependency graph is used. If cached package content does not match the locked hash, import resolution fails and instructs the user to run tya install.

Native package metadata lives under [native]. Native paths are relative to the package root. tya build, tya run, and tya test compile declared C sources with generated C, the Tya runtime, include directories, pkg-config flags, cflags, and ldflags. Native wrapper functions use the Tya runtime ABI and are called from package code like predeclared functions inside that package. Native packages are declarative; arbitrary shell build scripts in manifest metadata are not supported. Native packages compile and link trusted C/native code and are outside the safe-analysis trust boundary.

Package-provided tools live under [tools] and run through tya tool. Package tools are not global installs and are not shell tasks; they run from locked dependencies or an explicit one-shot git/path source.

Runtime and Concurrency

A script file is a lowercase .tya file and may be used as an entry file for tya run, tya build, and tya emit-c.

tya run hello.tya
tya build hello.tya -o hello
tya emit-c hello.tya

Class files are library-only and cannot be entry files.

Tya uses a compile-to-C pipeline for native execution. tya run compiles a temporary native executable, runs it, and removes the temporary executable. tya build -o PATH writes a reusable executable to PATH; without -o, tya build writes an executable in the current directory using the source basename. Intermediate native build artifacts live under .tya/build/. tya emit-c prints or writes the C program generated from Tya source. The generated C links against the Tya runtime.

The default native target uses the Tya-managed Zig toolchain as zig cc. Native package metadata from [native] contributes C sources, headers, include directories, pkg-config flags, compiler flags, and linker flags to the build.

WASM build targets are available where supported. Native packages are rejected for unsupported WASM targets. tya run remains native-only.

Cross Compilation

Cross-compilation is selected with --target on tya build. The native target is the default and uses the Tya-managed Zig toolchain as zig cc. WebAssembly targets produce artifacts for a different execution environment without running the program.

Current targets include:

Typical commands:

tya build --target native src/main.tya -o app
tya build --target wasm32-wasi examples/wasm/hello.tya -o hello.wasm
tya build --target wasm32-browser examples/wasm/hello.tya -o hello.wasm

tya doctor wasm reports the WebAssembly build environment and selected Zig path/version. tya doctor native reports the native build environment and selected managed zig cc path/version. Native package metadata may contribute C sources and linker flags for native builds, but packages with unsupported native requirements are rejected for unsupported WebAssembly targets.

WebAssembly builds preserve the compile-to-C backend and use the same Zig resolver as native builds. The first WebAssembly target layer supports stdout-oriented smoke programs. Browser builds also reject filesystem and process-oriented imports. tya run is native-only and does not execute WebAssembly artifacts. WebAssembly is documented for v1.0.0 but is not a release-blocking target; WASM-specific gaps are tracked separately from the core language release gates.

Errors and Diagnostics

Tya has two related error systems:

Compiler diagnostics use stable codes such as TYA-E0015 and linter diagnostics use stable codes such as TYAL0001. Diagnostics should include an actionable message and, where practical, a hint and documentation URL.

Runtime kind errors, invalid operations, failed assertions, failed I/O, and native wrapper errors are represented as Tya error values or raised runtime errors according to the API being used.

Standard-library failure behavior is part of each public API contract. Invalid argument kinds or arity are runtime errors. Absence and lookup APIs return nil only where documented. Operations that can fail because of the outside world, such as file, process, network, parse, compression, digest, time, random, native-wrapper, and serialization APIs, raise structured error values. Public v1.0.0 APIs do not use value, err pairs as the failure convention. Multiple return values remain valid for ordinary successful values.

raise may raise only error values. Raising nil, strings, numbers, dictionaries, or other non-error values is invalid because they do not carry the language error kind. A catch binding receives the exact raised error value.

Built-In Tools

The tya binary contains the compiler, formatter, language server, test runner, linter, documentation generator, package manager, project scaffolder, task runner, doctor commands, and package tool runner. Tool commands share the parser, checker, formatter, package resolver, and diagnostic conventions where applicable.

run       compile and execute a lowercase script entry as a temporary native executable
build     compile a reusable executable or target artifact; accepts --target and -o
emit-c    emit generated C
check     parse, load imports, and validate source without executing or invoking C
format    emit Formatted Syntax; --check reports drift; -w rewrites files in place
test      discover and run unittest tests; --cover records coverage
cover     render coverage profiles as text, JSON, or HTML
lint      report project-policy diagnostics for valid programs
lsp       run the JSON-RPC language server on stdio
doc       extract source-comment documentation; may emit JSON or static HTML
new       scaffold projects and libraries
task      list or run tya.toml tasks
install   resolve dependencies and write tya.lock
update    refresh locked dependency versions
add       add manifest dependencies
remove    remove manifest dependencies
outdated  report dependencies with newer versions
tool      list or run package-provided tools
doctor    report native or WebAssembly build environment details
embed     inspect embedded asset declarations
version   print the installed Tya version

tya clean removes .tya/build/ and keeps .tya/packages/. tya clean --packages also removes the project-local dependency cache. Project-local .tya/packages/ defines dependency meaning; global caches may optimize fetches but must not change resolution.

tya version --json reports compiler, runtime, SPEC, and self-host version metadata.

V1 Public Contract

All user-facing failures use the single stable TYA-E.... diagnostic-code namespace across lexer, parser, checker, codegen, runtime, CLI, LSP, formatter, package manager, stdlib, release, and bootstrap tooling. Runtime structured error values may additionally carry domain-specific kind and machine-readable code fields, but CLI human output, CLI JSON output, LSP diagnostics, and runtime failure reporting surface the same TYA-E.... diagnostic code for the same user-facing failure.

The v1.0.0 stdlib blocker set is part of the public release gate: regex/Regex, filesystem utilities in file/File and dir/Dir, time/Time, environment and process APIs in os/Os and process/Process, and hmac/Hmac must be implemented, documented, and covered before v1.0.0.

Compiler introspection compatibility is intentionally narrow. Stable v1 APIs are the documented entry points such as Lexer.lex, Parser.parse, Checker.check, Format.format, and explicitly documented AST helper methods. Full AST dictionary shapes, checker internals, and implementation helper fields are not v1 compatibility guarantees unless this specification documents them. Tooling may expose more data, but undocumented fields may change across v1.x releases.

Platform-dependent stdlib packages are importable on every supported release platform. Unsupported operations fail only when called, and those failures are structured errors with stable TYA-E.... diagnostics. The v1 release gate is repo-internal: Go tests, testscript fixtures, shell scripts, and release packaging checks are used for the release, but v1.0.0 does not add or promise a stable public tya conformance command.

Legacy compatibility aliases that remain only for selfhost/v01 or bootstrap recovery are legacy compatibility only. Public docs prefer canonical class-style stdlib APIs and canonical language spellings; legacy aliases are not v1.x compatibility guarantees.

Only tya install and explicit update/add package operations perform dependency network access by default. tya check, tya run, tya build, and tya test do not fetch missing dependencies automatically; if a required locked dependency is unavailable locally, they fail with an explicit error.

tya check and tya doc are safe-analysis commands: they parse, inspect, and report without executing user code or compiling native code. tya run, tya build, and tya test execute or build user code and may compile/link native package code.

tya lint diagnostics are warnings, not language validity errors. Suppression comments are # tya-lint-ignore: CODE for one line or next statement and # tya-lint-ignore-file: CODE, CODE for a file. Omitting codes suppresses all rules for that target. --fix may rewrite only rules with declared autofix. JSON findings include code, title, message, path, line, col, autofixable, and doc_url. SARIF and LSP diagnostics use the same stable rule codes, titles, help URLs, and warning severity. Public rule documentation lives at docs/lint.md and https://tya-lang.org/lint.html#tyal000N.

Current lint rules are:

TYAL0001 unused local binding              autofix
TYAL0002 dead code after return or raise
TYAL0003 redundant constant if             autofix
TYAL0004 deeply nested block
TYAL0005 long function body
TYAL0006 suspicious for-index binding order
TYAL0007 unused function parameter
TYAL0008 shadowed binding

tya test discovers only files whose basename ends in _test.tya. Ordinary .tya files are not auto-discovered as test files. Directory test discovery is deterministic: files run in ascending path order, and tests inside one file run in definition order. Tests are not run in parallel unless a future explicit option adds that behavior.

tya check reports every recoverable checker diagnostic it can collect and exits with status 1 when validation fails. If parser recovery is not possible, tya check may stop at that parser error and still exits with status 1. tya check - reads source from standard input and uses <stdin> when reporting diagnostics for stdin source.

tya run - reads source from standard input. Relative imports for stdin source resolve from the current working directory. Program arguments for tya run may follow --: tya run file.tya -- arg1 arg2 makes args() return ["arg1", "arg2"]. The legacy form tya run file.tya arg1 arg2 remains accepted for compatibility. tya build does not accept program arguments.

tya format never rewrites a file that cannot be lexed, parsed, and serialized by the formatter. Invalid source reports an error and exits without modifying the input file. tya format formats only .tya source files and never rewrites tya.toml.

LSP diagnostics use the same stable diagnostic codes and messages as the parser, checker, and linter diagnostics used by CLI tools. LSP may transport diagnostics in LSP shape, but it must not invent alternate wording for the same source validity issue.

--json is accepted as a global alias for JSON diagnostic output. JSON diagnostics use the existing stable NDJSON schema: diagnostic objects followed by a summary object. --format=json remains accepted for compatibility. Human-readable diagnostic color follows the existing color controls: NO_COLOR=1 and --no-color disable color, and --color=auto|always|never remains accepted for compatibility.

Ordinary emit-c output is stable for the same source and toolchain version. It does not include absolute paths, timestamps, random identifiers, or other nondeterministic metadata unless a future debug option explicitly requests it.

v1.0 has no experimental language feature gates. New syntax is not added in ordinary v1.x releases; v1.x may add standard-library APIs, package APIs, tooling improvements, diagnostics, and compatible runtime behavior. New syntax requires v2 planning or an explicitly accepted experimental feature path outside the stable v1 public contract. Experimental syntax or --experimental-* tool options are invalid unless a later accepted SPEC adds that behavior explicitly.

Environment variables are read by user programs only through explicit calls such as env(name) or documented standard-library APIs. .env files are not loaded automatically; users or task-runner commands may load them before invoking Tya. Toolchain behavior may be affected by the documented variables TYA_PATH, TYA_LIB_DIR, TYA_STDLIB_DIR (deprecated fallback), TYA_RUNTIME_DIR, TYA_PROJECT_ROOT, TYA_ZIG, TYA_ZIG_HOME, TYA_ZIG_VERSION, TYA_ZIG_SHA256, TYA_DISABLE_ZLIB, TYA_ENABLE_ZLIB, TYA_DISABLE_OPENSSL, TYA_ENABLE_OPENSSL, CC, NO_COLOR, and HOME in isolated test/toolchain contexts.

tya doc extracts leading source comments attached to public top-level functions, classes, modules, interfaces, public class variables, public class constants, public methods, static methods, and interface methods. A doc comment attaches only to the immediately following public item; a blank line breaks attachment and leaves the comment orphaned. With no path it scans src/. It can render terminal text, generate static HTML, or emit a stable JSON report:

tya doc
tya doc src
tya doc --json src
tya doc --html ./out src

Doc comment metadata uses tag lines inside the leading comment block. `@type

` documents a variable or constant type hint. `@param ` documents callable parameters. `@return ` documents return values. `@option . ` documents dictionary option keys. Metadata tags are documentation only; they do not affect parsing, checking, code generation, runtime behavior, or formatter output. Type hints are free-form display strings. The JSON report contains `version`, `items`, and `diagnostics`. Each item includes its name, kind, signature, raw Markdown comment, rendered text, metadata fields, source path and line, and `reexported_from` when the item is included through an imported package surface. Documentation extraction follows public re-exports through imports, reports import cycles without hanging, and keeps ordering deterministic. Documentation diagnostics use stable `TYADOC` codes. Orphan doc comments, duplicate public documentation names, unsupported Markdown bodies, import cycles, unknown metadata tags, duplicate metadata tags, malformed metadata tags, and `@param` tags that do not match callable parameters are reported to stderr for text and HTML output and embedded in JSON output. Error diagnostics exit with status 1 after output is produced; warning diagnostics do not block output. Argument, path, I/O, lex, and parse failures exit with status 2. `tya task` lists and runs project-local shell tasks declared under `[tasks]` in `tya.toml`. It discovers the project manifest by walking up from the current directory and runs commands with the project root as the working directory. Task entries may be strings, arrays of strings, or table forms: ```toml [tasks] run = "tya run src/main.tya" check = ["tya format --check src", "tya check src/main.tya"] [tasks.dev] depends_on = ["check"] env = { TYA_ENV = "dev" } cmds = ["tya run src/main.tya"] [tasks.watch] cmds = ["tya check src/main.tya", "tya lint src"] parallel = true watch = ["src/**/*.tya", "tests/**/*.tya"] ignore = ["tmp/**", "dist/**"] ``` String tasks run as one `/bin/sh -c` command. Array tasks run sequentially and stop on the first failing command. Table tasks use `cmds = [...]`; when `parallel = true`, commands run concurrently and line output is prefixed with the command index. `depends_on` runs dependency tasks before the selected task, once per invocation, in the order written. Dependency cycles and unknown dependencies are reported before any task command runs. `env` values override the inherited process environment for that task only. Dependencies use their own environment and do not inherit the selected task's overrides. `tya task --watch` runs the task once and reruns it after watched project files change. By default, watch mode observes `.tya` files, `tya.toml`, and files under existing `src/`, `tests/`, `lib/`, and `examples/` directories, while ignoring `.git/`, `node_modules/`, `_site/`, common build output directories, and hidden cache directories. Table-form `watch` overrides the default watch set and `ignore` adds ignored globs. `--watch` is consumed by the task runner before `--`; arguments after `--` are passed to the task command. ### Verification Commands Verification commands inspect source and report whether it satisfies a specific contract. They do not define language syntax or standard-library behavior. Verification commands include `tya format --check`, `tya check`, `tya lint`, `tya test`, and future `tya verify`. `tya run` and `tya build` may share diagnostics and exit-code conventions with verification commands, but they are execution and build commands. `tya format --check` checks whether source files already match Formatted Syntax. It answers whether `tya format` would change the file. It must not rewrite files. `tya check` checks whether source files are valid Tya programs before C emission or execution. It includes lexical analysis, parsing, semantic checking, and import loading needed for the requested program. It excludes C emission, C compiler invocation, executable creation, program execution, unit test execution, and lint rules. `tya lint` checks rules that are not required for language validity. A program that fails only lint rules is still a valid Tya program. Lint rules may be built in, configured by a project, or added later by tooling. `tya test` is the execution entry point for unittest-based tests. It reports passed tests, failed assertions, skipped tests when supported, runtime errors, and test discovery errors. `tya verify` is reserved for the standard verification pipeline. Its order is: ```text format --check -> check -> lint -> test ``` Initial implementations may run only the commands that exist at the time. Before `tya verify` exists, CI may run: ```sh tya format --check . tya check . ``` Verification commands use stable exit-code meanings: ```text 0 verification passed 1 verification failed 2 command usage error 3 internal tool error ``` Verification commands accept explicit file and directory targets. Directory targets recursively select `.tya` source files meaningful for the command. With no target, verification commands use the current directory unless the command has a stronger existing convention. Human-readable verification output is concise by default. Failures should include the command name, file path, line and column when available, a short rule or diagnostic name when available, and an actionable message. Multi-file commands should continue after ordinary verification failures where practical and then report a summary. `--quiet`, `--verbose`, and `--json` are reserved for consistent verification behavior. `--json` preserves the same pass/fail meaning and exit codes as human-readable output. Verification commands distinguish checking from rewriting. `tya format` may rewrite files. `tya format --check`, `tya check`, `tya lint`, `tya test`, and `tya verify` do not rewrite files by default. Automatic lint fixes require an explicit option such as `--fix`. ### Single Binary Distribution Tya is distributed as one primary `tya` binary. The binary contains the toolchain entry points and uses the bundled standard library and C runtime files that ship with the release. The one-binary model is part of the language's operational design: users should not need separate formatter, test runner, LSP server, doc generator, package manager, or build driver executables for normal Tya work. Releases may include support files such as the standard library, C runtime sources, editor assets, examples, or installation metadata, but the command surface is centered on the single `tya` executable. ## Standard Library The standard library is shipped with Tya under `lib/` and is imported using the same import syntax as user files and packages. The standard library is part of the language distribution. Its public surface is the set of importable PascalCase package classes, interfaces, structs, and records from snake_case type files under `lib/`. Standard-library imports are resolved after local packages, locked package dependencies, and `TYA_PATH` entries. Public standard-library classes, interfaces, structs, records, and user-facing methods carry source doc comments. Generated stdlib API documentation is produced from those comments with `tya doc`, for example `tya doc --json lib` for a machine-readable reference that includes package paths, signatures, rendered comments, source paths, and source lines. Current standard-library surface: ```text base64/Base64 Base64 encode/decode helpers binary/Reader binary input reader binary/Writer binary output writer channel/Channel native channel value option_parser GNU-style command-line option parser collections/Deque double-ended queue collections/PriorityQueue priority queue collections/Queue FIFO queue collections/Set set collection collections/Stack LIFO stack color/Color RGBA color value and conversions compiler/ast/Ast compiler AST helpers compiler/checker/Checker compiler checker helpers compiler/format/Format compiler formatter helpers compiler/lexer/Lexer compiler lexer helpers compiler/parser/Parser compiler parser helpers compress/Codec compression codec interface compress/Gzip gzip compression helpers compress/Zlib zlib compression helpers csv/Csv CSV parse/generate helpers digest/Digest digest/hash helpers dir/Dir directory helpers file/File file helpers geometry/Circle circle value geometry/Point point value geometry/Rect rectangle value geometry/Size size value geometry/Vector2 2D vector value geometry/Vector3 3D vector value hex/Hex hexadecimal encode/decode helpers hmac/Hmac keyed message authentication helpers image/Codec image codec helpers image/Image image value io/Io stream helpers io/Reader readable stream wrapper io/Writer writable stream wrapper json/Json JSON parse/generate helpers log/Logger logger markdown/Markdown Markdown renderer math/Math numeric helpers regex/Regex regular expression helpers matrix/Matrix matrix value net/http/Client HTTP client net/http/Next HTTP middleware continuation net/http/Server HTTP router/server net/ip/Address IP address value net/ip/Network IP network value net/socket/Server socket listener net/socket/Socket socket connection os/Os operating-system helpers path/Path path manipulation helpers process/Process process helpers random/Random random helpers random/Rng seeded random generator runtime/Runtime runtime introspection helpers secure_random/SecureRandom secure random helpers serialization/Serializer data serialization helpers sync/AtomicInteger native atomic integer sync/Mutex native mutex sync/WaitGroup native wait group task/Task task helpers template/Template template renderer time/Time time value and time helpers toml/Toml TOML parse/generate helpers transform2d/Transform2D 2D affine transform value unittest/TestCase test case base class unittest/TestRunner test runner unittest/TestSuite test suite url/Url URL parse/build helpers validation/NumberValidator numeric validation helpers validation/StringValidator string validation helpers value/Value value introspection helpers xml/Xml XML parse/generate helpers xml/Document XML document node xml/Element XML element node xml/Text XML text node xml/CData XML CDATA node xml/Comment XML comment node ``` Current standard-library protocol and sequence helper files: ```text comparable Comparable interface equatable Equatable interface stringable Stringable interface iterator Iterator interface; requires has_next() and next() iterable Iterable interface; requires iter() sequence Sequence class and chainable sequence protocol iterable_sequence sequence wrapper for Iterable values map_sequence lazy map sequence filter_sequence lazy filter sequence take_sequence lazy take sequence drop_sequence lazy drop sequence ``` `Comparable` requires `compare(other)` and provides `lt?`, `lte?`, `gt?`, `gte?`, and `between?`. `Equatable` requires `equal?(other)`. `Stringable` requires `to_string()`. Debug display uses `inspect()`. `Iterable` requires `iter()` and provides `sequence()`. `Sequence` provides `iter()`, `map(fn)`, `filter(fn)`, `take(n)`, `drop(n)`, `reduce(initial, fn)`, `each(fn)`, `any?(fn)`, `all?(fn)`, `find(fn)`, and `to_a()`. ### Environment And Process `os/Os` exposes process environment and working-directory helpers: `args()`, `env(name)`, `environ()`, `setenv(name, value)`, `unsetenv(name)`, `cwd()`, `chdir(path)`, and `exit(code)`. Environment names and values are strings. A missing environment variable returns `nil`, and environment mutation affects the current process and child processes started after the mutation. `process/Process.run(command, options = {})` runs a child process and returns a result dictionary. `command` may be an array of strings for direct execution or a string when `options["shell"] == true`. String commands without explicit shell opt-in are invalid. Supported options are `cwd`, `env`, `clear_env`, `stdin`, `capture_stdout`, `capture_stderr`, `timeout`, and `shell`. Unknown option keys are invalid. `env` values override the inherited environment unless `clear_env` is true. `stdin` may be a string or bytes value. The result dictionary contains `status`, `success`, `stdout`, `stderr`, and `timed_out`; `exit_code` remains as a compatibility alias for `status`. Non-zero child exit status is reported in the result dictionary and is not a raised error. Spawn/setup failures, invalid options, invalid environment values, timeout setup failures, and unsupported `Process.exec(command, options = {})` raise structured process errors. ### Filesystem Utilities `file/File().copy(src, dst, options = {})` copies file contents as bytes. Supported options are `overwrite` and `preserve_mode`, both defaulting to `true`. When `overwrite` is false and `dst` exists, the operation raises a filesystem error. `file/File().chmod(path, mode)` changes POSIX-like permissions where the platform supports them. Windows permissions are best-effort and unsupported permission changes raise filesystem errors rather than silently promising POSIX behavior. `file/File().temp(prefix = "tya", suffix = "")` creates an empty temporary file under the operating-system temporary directory and returns its path. `dir/Dir().mkdir_all(path)` creates a directory and missing parents. `dir/Dir().remove_all(path)` removes a file or directory tree recursively; missing paths are a no-op, while dangerous roots such as `""`, `"."`, `/`, and platform roots are invalid. `dir/Dir().temp_dir(prefix = "tya")` creates a temporary directory and returns its path. `dir/Dir().walk(path, fn, options = {})` visits a directory tree in ascending path order. `fn` receives an entry dictionary with `path`, `name`, `kind`, and `stat`. Supported options are `follow_symlinks`, `include_dirs`, and `include_files`; symlink following is not required where the host platform cannot safely detect loops. ### HMAC `hmac/Hmac` provides keyed message authentication without adding a broader cryptography suite to v1.0.0. `Hmac().digest(algorithm, key, message)` returns raw bytes. `Hmac().hexdigest(algorithm, key, message)` returns lowercase hexadecimal text, and `Hmac().base64digest(algorithm, key, message)` returns Base64 text. Supported algorithms are `sha256`, `sha384`, and `sha512`. `key` and `message` must be strings or bytes; strings are encoded as UTF-8 bytes. `Hmac().verify(algorithm, key, message, expected, options = {})` compares the computed digest to `expected` using constant-time comparison for equal length byte sequences. `expected` may be raw bytes, hex text, or Base64 text when `options["encoding"]` is `"raw"`, `"hex"`, or `"base64"`. Unsupported algorithms, malformed encodings, unknown options, and wrong kinds raise structured crypto errors. General encryption, public-key cryptography, password hashing, certificates, and streaming HMAC contexts are outside v1.0.0. ### Regex `regex/Regex` provides regular expression helpers without adding regex literals or operators to the language. `Regex().compile(pattern, options = {})` returns a reusable regex value. `Regex().match?(pattern, text, options = {})` returns whether the pattern appears anywhere in `text`, and `Regex().search(pattern, text, options = {})` returns the first match dictionary or `nil`. Compiled regex values support `match?(text)`, `find(text)`, `find_all(text)`, `split(text, limit = nil)`, and `replace(text, replacement, limit = nil)`. Match dictionaries contain `text`, `start`, `end`, and `groups`; `start` and `end` are Unicode rune indexes and `groups` is an array of captured strings or `nil` for unmatched optional groups. Replacement supports explicit numeric capture references with the `${1}` spelling; `$$` emits a literal dollar sign, and unknown capture references are invalid. Supported options are `ignore_case`, `multi_line`, and `dot_all`, all bools defaulting to `false`. The v1 portable regex syntax subset is common extended regular expression syntax: literals, `.`, character classes, grouping, alternation, `*`, `+`, `?`, bounded repeats, and `^`/`$` anchors. Lookbehind, backtracking-control verbs, locale-dependent matching, regex literals, and engine-specific extensions are outside v1.0.0. Invalid patterns, unknown options, wrong option kinds, wrong argument kinds, and invalid replacement captures raise structured regex errors. ### Time `time/Time` provides wall-clock times, monotonic timestamps, durations, formatting, parsing, arithmetic, and sleeping without adding date/time syntax. `Time().now()` returns the current wall-clock time. `Time().monotonic()` returns a monotonic timestamp for elapsed-time measurement; monotonic values may be subtracted or compared but cannot be formatted as wall-clock dates. `Time().unix(seconds, nanos = 0)` constructs a UTC wall-clock time. Wall-clock time values support `unix()`, `unix_nanos()`, `utc()`, `local()`, `format(layout)`, `add(duration)`, and `sub(other)`. `sub` returns a duration. `Time().parse(text, layout = "rfc3339")` parses documented layouts. Supported layout names are `rfc3339`, `date`, `time`, and `unix` for formatting, and `rfc3339`, `date`, and `unix` for parsing. `Time().duration(seconds = 0, options = {})` constructs a duration. Supported duration options are `minutes`, `hours`, `milliseconds`, `microseconds`, and `nanoseconds`. Duration values support `seconds()`, `milliseconds()`, `microseconds()`, `nanoseconds()`, `add(other)`, and `sub(other)`. `Time().sleep(duration_or_seconds)` accepts either a duration value or a number of seconds. Timezone support for v1.0.0 is limited to UTC and the process local timezone. Named timezone database lookup, locale-aware month/day names, date/time literals, recurrence APIs, and leap-second guarantees beyond host runtime behavior are outside v1.0.0. Invalid layouts, invalid parse text, wrong argument kinds, and unknown duration options raise structured time errors. `io/Reader`, `io/Writer`, and `net/socket` define stream capability interfaces for readable, writable, closable, and flushable values. `Reader` supports `read`, `read_line`, `each_line`, `eof?`, and `close`. `Writer` supports `write`, `write_line`, `flush`, and `close`. `Socket` supports `connect`, `read`, `read_line`, `write`, `write_line`, `close`, `closed?`, `local_address`, and `remote_address`; `net/socket/Server` supports `listen`, `accept`, `close`, and `local_address`. The compiled runtime supports `net/socket` on POSIX socket platforms and Windows through WinSock2. `net/http/Server` defines route registration by HTTP method (`get`, `post`, `put`, `delete`, `patch`, `options`, `head`, `any`), middleware (`use`, `group`), error and not-found handlers, static-file serving, redirects, route dispatch, and server execution. `net/http/Client` defines `get`, `post`, and generic `request`. `net/http/Client` accepts both `http://` and `https://` URLs. HTTPS uses the compiled runtime's OpenSSL backend. Certificate verification is enabled by default; request options may set `ca_file` to a PEM CA bundle or `insecure_skip_verify: true` to disable verification explicitly. TLS failures raise `http.tls:` or `http.request:` errors. `net/http/Server.run_tls(port, cert_file, key_file, options)` serves HTTPS using PEM certificate and private key files; options may include `host` and `timeout`. Building TLS-enabled programs requires OpenSSL headers and libraries. Compiled `net/http/Server` handlers receive request dictionaries with `cookies`, a dictionary parsed from the incoming `Cookie` header. Missing cookies produce `{}`. Malformed pairs without `=` are ignored, whitespace around names and values is trimmed, and repeated names keep the last value. Handlers also receive `form` and `files` dictionaries. For non-multipart requests both are empty. For `multipart/form-data` requests, `form` maps field names to the last string value and `files` maps field names to the last uploaded file metadata dictionary. File metadata contains `filename`, `content_type`, `body` as bytes, and `size`. The original raw request body remains available in `body`. Malformed multipart bodies return `400 Bad Request` before the handler runs. `Server.cookie(name, value, options)` formats a `Set-Cookie` header value. Options may include `path`, `domain`, `max_age`, `expires`, `secure`, `http_only`, and `same_site` (`Lax`, `Strict`, or `None`). `SameSite=None` requires `secure: true`. `Server.with_cookie(response, name, value, options)` appends a cookie to `response["header_values"]["Set-Cookie"]`. Response dictionaries may use `header_values` for repeated response headers; each array entry is emitted as a separate header line while ordinary `headers` behavior is unchanged. `Server.render(template, data, options)` and `Server.render_html(template, data, options)` return response dictionaries with rendered HTML bodies. `options` may be `nil`; the default response status is `200` and the default `Content-Type` is `text/html; charset=utf-8`. Supported options are `status`, `headers`, `content_type`, and `template_options`. String templates that name an existing file are rendered with `template.Template.render_file`; other strings are rendered as template source. Embedded bytes are decoded as UTF-8 text before rendering. `render_html` forces HTML escaping even when `template_options` is present. Extra headers are merged after defaults, so callers may override `Content-Type`. Response dictionaries may set `chunked: true` to send an HTTP/1.1 chunked response. In that mode the runtime writes `Transfer-Encoding: chunked`, omits `Content-Length`, and writes each string or bytes item from an array body as one chunk. A channel body may yield string or bytes chunks and closes the stream when the channel closes. Empty chunks are skipped except for the final terminating chunk. Non-chunked responses keep the normal `Content-Length` behavior. HTTP/1.1 server connections default to keep-alive unless the request contains `Connection: close`. HTTP/1.0 connections default to close unless the request contains `Connection: keep-alive`. Request dictionaries expose `keep_alive` as the boolean decision for that request. Responses include `Connection: keep-alive` or `Connection: close`, and each accepted connection is limited to a conservative maximum number of requests. `serialization/Serializer` converts Tya values to and from data values, JSON, and TOML. Classes that implement `Serializable` expose `to_data()`. ### External Packages External packages and tools are distributed outside this repository and are consumed by git URL plus tag, branch, or revision through `tya.toml`. Known public packages and tools include: - `https://github.com/komagata/tya-sqlite` - `https://github.com/komagata/tya-sdl2` - `https://github.com/komagata/tya-gtk4` - `https://github.com/komagata/tya-raylib` - `https://github.com/komagata/tya-slim` - `https://github.com/komagata/flakewatch` - `https://github.com/komagata/magvideo` ## Distribution and System Considerations Tya programs compile to C and link against the Tya runtime. The runtime provides value representation, garbage collection, primitive methods, class dispatch, task and channel support, resources, and native wrapper integration. The implementation must preserve the self-host fixed-point invariant documented in `ROADMAP.md`: the maintained Tya-written compiler under `selfhost/v01/` must continue to compile itself to stable stage-2 and stage-3 output. The compiler front end is hand-written. Parser generators and large grammar frameworks are not language authority for the active compiler path.