Tya v0.28 Specification
This document is the specification for Tya v0.28 after v0.27 hexadecimal and binary integer literals.
Theme
Tya v0.28 turns the project's strict-lint goals into compile errors by default.
The Tya brief from the start lists "lint-like rules as compile errors" as a core principle. v0.28 adopts the most useful subset and makes them mandatory: shadowing, unused imports, unused function arguments, and unused private top-level definitions.
The effect is fewer silent bugs at the cost of slightly more discipline at write time. Where intent is genuinely "I do not need this name," the discard form _ makes it explicit.
Goals
- Reject variable shadowing.
- Reject unused
importbindings. - Reject unused function arguments (with
_opt-out). - Reject unused private top-level definitions (names starting with
_). - Make these checks default compile errors, not opt-in.
- Keep migration straightforward: the discard underscore covers each case.
Included in v0.28
v0.28 includes all v0.27 behavior and adds:
- **Shadowing**: a name introduced in a nested scope must not match a
binding visible from the enclosing scope.
- **Unused imports**:
import foo(with or withoutas) is an error if
the binding is never read.
- **Unused function arguments**: a parameter that is never referenced in
the function body is an error unless its name is _ or starts with _.
- **Unused private top-level definitions**: a top-level binding whose
name starts with _ and which is never referenced is an error.
Not Included in v0.28
v0.28 does not include:
- Unused **local** variables as a compile error (the existing
--check-unused opt-in flag remains; it is not promoted to the default in v0.28 to avoid a wave of churn in third-party code).
- Tab-character-in-indentation as a compile error (already rejected at
lexer level; this is unchanged).
- Trailing-whitespace lint.
- "Top-level executable code only in
main.tya" enforcement (a separate
breaking change to entry-program semantics, deferred).
- Naming-convention enforcement beyond what already exists (snake_case
file names, predicate ? rules from v0.19, private _ prefix; v0.28 does not add new naming bans).
- Duplicate-definition tightening beyond what already exists.
Shadowing
A binding *shadows* another when a nested lexical scope introduces a name that is visible from a strictly enclosing scope.
Tya assignment (name = value) reassigns an outer-scope binding when one already exists, and otherwise creates a new local. Because = does not declare a fresh binding unambiguously, **assignment statements are not subject to the shadow check**. Shadowing is reported only for binding forms that explicitly introduce a new name in a nested scope: for loop variables and try ... catch bindings. Function parameters are also exempt (see "Function parameters and shadowing" below).
Forbidden examples
count = 0
for count in items # error: loop variable shadows outer count
print count
err = "outer"
try
risky()
catch err # error: catch binding shadows outer err
print err
Allowed examples
greet = ->
x = 2
print x
x = 1
print x
# Function parameters MAY share a name with an outer binding.
user = { name: "komagata" }
has_name? = user -> has(user, "name")
print has_name?(user)
# Sibling scopes do not see each other.
process = ->
total = 0
for item in items
total = total + item
total
count = ->
total = 1
total
Function parameters and shadowing
Function parameters introduce names in the function's own scope, but for shadowing checks parameters are exempt from the rule. A function may take an argument with the same name as an outer binding:
user = current_user()
greet = user -> # ok: parameter user shadows outer user without error
print user
This exemption matches the typical "outer name describes a value, parameter describes a parameter of that kind" idiom from CoffeeScript / Ruby / Python. Body-local bindings (assignments, loop variables, catch bindings) still participate in the shadow check against both the parameter scope and the enclosing scope.
A binding can be reassigned in the **same** scope without error:
x = 1
x = 2 # ok: same scope, not a new binding
What counts as a binding
The following introduce a new binding for the purpose of shadowing checks:
- Top-level
name = value - Function parameters (
name = arg1, arg2 -> ...) for x in xsandfor x, y in xsloop variablesfor k, v of objvariablestry ... catch name ...catch binding- Assignment statements inside a nested scope where the name is not
already bound in that same scope
Underscore discards do not shadow
Names that are exactly _ are not bindings. Multiple _ in the same scope are allowed, and _ does not collide with any other name. Names that begin with _ (private discards) are bindings and **do** participate in shadowing checks.
Unused Imports
import string # error: unused
import array
import string as s # error: unused
print array.first(items)
A import is unused when no expression in the importing file references the bound name (the alias if as is used, otherwise the module name).
Opt-out
There is no opt-out. The fix is to remove the import. Importing for side effects is not a Tya idiom; import only binds a module.
Unused Function Arguments
greet = name -> "Hello, world" # error: name is unused
handler = event ->
log("hit")
nil
# fix 1: rename to underscore prefix
handler = _event ->
log("hit")
nil
# fix 2: rename to bare underscore
handler = _ ->
log("hit")
nil
# fix 3: actually use the argument
greet = name -> "Hello, {name}"
Method receivers
The implicit @ receiver is not a parameter and is not subject to this check.
Class abstract method declarations
Abstract method declarations (abstract method = a, b -> without a body) are signature-only. Their parameter names are not checked for use.
Pattern parameters (destructuring)
Destructuring parameters are checked at the level of bound names. If you destructure a value but never use one of the names, that name needs _:
process = [first, _second, third] ->
use(first, third)
(Destructuring as a parameter form is not introduced in v0.28; this clarification is forward-compatible.)
Unused Private Top-Level Definitions
# private_helper is never used in this file
_private_helper = x -> x + 1
print "main"
Top-level bindings whose names start with _ are private to their file. v0.28 makes "defined but never used" a compile error for these names so that dead helper functions are caught.
Opt-out
Rename without the leading underscore (and accept it becomes part of the file's public surface), or delete the definition. There is no inline suppression.
Migration
Existing code that does not satisfy the new rules has three migration options for each violation:
| Violation | Fix | |---|---| | Unused import | Remove the import | | Unused arg name | Rename to _name or _ | | Unused private _helper | Remove or actually call it | | Shadowing inner binding | Rename inner binding | | Shadowing loop variable | Rename loop variable |
The Tya v0.28 release migrates the standard library, examples, and tests internally. Third-party code may need a one-time pass.
CLI
The existing --check-unused flag retains its current behavior (warns on unused **local** variables, which v0.28 does not promote). All checks described above are emitted as compile errors by every command that does type/scope checking (tya check, tya run, tya build, tya emit-c, tya test, tya fmt -w).
tya fmt without -w is a syntax-only formatter and does not run the new strict checks.
Diagnostics
v0.28 implementations should report source-oriented errors with one of these messages (or close equivalents):
shadows outer binding <name>— points at the inner bindingunused import <name>— points at theimportstatementunused argument <name>— points at the parameterunused private top-level definition <name>— points at the binding
Diagnostics should mention the line and column and the offending name.
Implementation Notes (non-normative)
- Shadowing: the checker maintains the chain of enclosing scopes. When
binding a new name, walk the chain (excluding the current scope) and reject if any frame holds the same name.
- Unused tracking: each scope records
(declared, read)sets per name.
At scope exit, names in declared minus read are diagnosed.
- Underscore: treat the bare name
_as a fresh scope-anonymous binding
that no read can target.
- Top-level private: the checker scans assigned names starting with
_
before the body and counts a use whenever any expression contains an identifier matching the name.
- Tests: each rule's positive / negative case in
tests/strict_lint_test.tya plus targeted txtar.
These notes are guidance, not the spec.