Language

Types, records, enums, and recoverable errors

Available in 0.3.0-dev. Existing unannotated code stays dynamic. An annotation is now a checked contract, rather than a comment to the interpreter.

enum Status { active, completed }
record Project {
    name: String,
    status: Status,
    description: String?
}

fn title(project: Project) -> String { return project.name }
let project: Project = Project({name: "Zy", status: Status.active})
print(title(project))
print(project.description) // null: an omitted optional field
print(project.status.value) // active

Gradual contracts

Annotations are supported on let, function parameters, and function results (-> Type). Primitive names are Int, Float, String, Bool, Null, List, Map, Function, IP, Subnet, Agent, and Error; Any accepts any value. Type? accepts that type or null. Types are case-sensitive. Int and Float are distinct: there is no implicit numeric conversion at a typed boundary.

Variable reassignment and compound assignment retain the declared contract. Functions check arguments before entering the body and check returned values, including the implicit null on fallthrough. Contracts capture the declared record/enum identity; a different type with identical fields is not compatible. Types must be declared/imported before their annotations are used.

List and Map check the outer kind, not element types. They remain mutable. There are no generic collections, union types, interfaces, or inheritance yet. This release does not claim full static typing or exhaustive enum matching.

Records and enums

record Name { field: Type, ... } creates a constructor. Pass exactly one Map with the declared fields. Missing required fields, unknown fields, and wrong types raise ZY2000. Optional omitted fields become null. Fields are readable through .field and cannot be reassigned or removed. The constructor copies the outer field map; nested List/Map values remain shared and mutable.

enum Name { variant, ... } creates distinct named values. Compare variants directly (Status.active == Status.active). .value returns the variant String. Enums do not have constructors or payloads. JSON encodes records as objects and enum variants as their String value. JSON decoding returns plain Maps/Strings; use an explicit record constructor and enum conversion to validate those inputs.

Both declarations support export. Imported annotations use module.Type:

import "./models.zy" as models
fn save(project: models.Project) -> models.Project { return project }

Recoverable errors

fn read_config(path: String) -> Map {
    try {
        return json.load(path)
    } catch error {
        print(error.code, error.message)
        throw error
    }
}

try { throw "Invalid deployment configuration." } catch error {
    print(error.kind, error.line)
}

try { ... } catch name { ... } catches runtime errors, including file, HTTP, database, type, assertion, and explicitly thrown errors. The catch binding is an Error value scoped to its handler. Error fields are code, kind, message, file, line, column, hint, and stack. stack is a List of call frames containing function, file, line, and column. Frames appear from the innermost call outward. Diagnostic category codes retain their existing meaning.

throw accepts a String or a caught Error. Rethrowing preserves the original location and stack. Return/break/continue propagate normally through try blocks. There is no finally construct yet. Cancellation and evaluator/call-depth exhaustion cannot be swallowed by catch. Parse/module-loading failures happen before execution and cannot be caught.

What check can prove

zy check and editor diagnostics check known annotations, literal assignments, typed calls/results, record constructors, fields, enum variants, and imported exports without execution. They detect obvious missing return paths. Untyped bindings, mutable collection contents, computed indexes, and host/native results remain dynamic and are checked at runtime when they cross a typed boundary. There is no nullable flow narrowing; use a dynamic local after a guard when necessary. The checker reports the first error and does not run input validators.

The interpreter still permits catching runtime contract violations. A known literal violation may be rejected by zy check even if placed inside try; use dynamic input when testing runtime rejection behavior.

Found something to improve?Edit this page on GitHub ↗
Search guides, language features, and APIs ↵ to open first result