Language specification
The formal definition of TB 3.2.0: lexical structure, grammar, values, statements, classes, errors, generators, patterns and formatting. For a gentle introduction, start with the guide.
Status: final for TB 3.0.0. Every rule here was checked against the reference implementation (tbcore/: lexer, parser, tree-walking interpreter and bytecode VM). The open design questions of the draft were settled by what tbcore actually does; those places are marked (settled) with the answer. The conformance tests in spec/*_test.tb check the rules and run with tb test spec.
TB is a small, friendly, dynamically typed language with gradual type annotations (parsed and kept, ignored when the program runs; tb check uses them). Programs are UTF-8 text files ending in .tb. The reference implementation is in Python; tb file.tb runs a program.
Contents
- Lexical structure
- Grammar (EBNF)
- Values and types
- Expressions and operators
- Statements
- Functions
- Classes, enums and modules
- Errors
- Generators, iteration and tasks
- Patterns (
match) - Formatting (
f"...",format) - Program structure, imports and packages
- Compatibility with TB 2.x
1. Lexical structure
1.1 Source text
A program is a sequence of Unicode characters. Lines end in \n; a \r is treated as white space, so \r\n files work. Indentation has no meaning. Statements are separated by newlines or ;. Newlines are otherwise not significant inside an expression: a list, map or call may span several lines, and an expression may continue on the next line after a binary operator. Four places do look at line breaks:
- A
(or[that starts a new line begins a new statement; it never calls or indexes the expression on the line before. return,throw,yield,importandbreak/continuetake their value (or end) on the same line:returnalone on a line returns nil.- Inside a
match, an operator at the start of a line ends the current arm (so-1 => ...on the next line is a new arm). - A comment that follows code on the same line is that line's trailing comment (kept by
tb fmt).
1.2 Comments
comment = "#" { any character except newline } .
doc-comment = "##" { any character except newline } .
Comments run to the end of the line. A ## block directly above a fn, class (or one of its members), enum, const, let or export declaration is its documentation; tb doc FILE.tb and the language server show it. Without a ## block, the plain # block directly above is used. The comment block at the very top of a file, followed by a blank line, describes the file. The parser keeps comments on statement nodes (comments, blank_before, trailing_comment) so tb fmt can put them back.
1.3 Identifiers and keywords
identifier = ( letter | "_" ) { letter | digit | "_" } .
letter = any Unicode letter .
digit = "0" … "9" .
Reserved words — using one as a name is an error with a suggested new name ('match' is a keyword in TB 3.0; call it 'found' instead.):
and async await class const else enum export
extends false finally fn for from if in
is let match nil not or return self
static super then true while yield
Statement words (settled): import as try catch throw break continue defer assert are not reserved. They are special only where they start a statement (or, for as, inside an import), so 2.x programs that use them as names keep working: let import = 3 is fine.
Notes
doneis a built-in constant (the iterator sentinel), not a keyword; like every built-in it can be shadowed (settled)._is an ordinary identifier except in amatchpattern, where it matches anything without binding.- Dot access is never affected by keywords:
re.match,obj.fromare fine.
1.4 Literals
number = int-literal | float-literal .
int-literal = dec-int | "0x" hex-digit { hex-digit | "_" }
| "0b" bin-digit { bin-digit | "_" } | "0o" oct-digit { oct-digit | "_" } .
dec-int = digit { digit | "_" } .
float-literal = dec-int "." digit { digit | "_" } [ exponent ] | dec-int exponent .
exponent = ( "e" | "E" ) [ "+" | "-" ] digit { digit } .
_ separates digits (1_000_000); a number may not end with _. 1e3 and 2.5e-3 are floats. Int literals have unlimited size. A number needs a digit before the dot (0.5, not .5). There is no unary +.
text-literal = [ prefix ] ( short-text | long-text ) .
prefix = "f" | "r" | "b" | "rf" | "fr" | "rb" | "br" (either case) .
short-text = '"' { char | escape | hole } '"' | "'" { char | escape | hole } "'" .
long-text = '"""' { char | newline | escape | hole } '"""'
| "'''" { char | newline | escape | hole } "'''" .
escape = "\n" | "\t" | "\r" | "\0" | "\\" | '\"' | "\'"
| "\x" hex-digit hex-digit | "\u{" hex-digit { hex-digit } "}" .
hole = "{" expression [ "!r" ] [ ":" format-spec ] "}" . (f-text only)
- Plain text: escapes are processed; any other
\cis kept as typed (so regular expressions like"\d+"work). A short text may not contain a raw newline. r"..."raw text: no escapes (r"\d+"is three characters).f"...": holes are evaluated and inserted;{{and}}are literal braces; an empty{}is an error. A hole may contain quotes and brackets.b"..."bytes: ASCII characters and escapes only;b"é"is an error.\u{...}takes 1 to 6 hex digits, up to10FFFF.
1.5 Operators and punctuation
+ - * / // % ** & | ^ ~ << >> == != < <= > >=
= += -= *= /= //= %= **= &= |= ^= <<= >>=
.. ..= ... => -> . , : ; ( ) [ ] { } !
Word operators: and or not is in, and the two-word not in and is not (settled: both exist). !x is accepted as a tight-binding not x (!a == b is (not a) == b). && and || are errors that point to and/or.
2. Grammar (EBNF)
Conventions: { x } repetition, [ x ] optional, ( a | b ) choice, terminals in quotes. SEP is a newline or ; (several in a row are fine).
program = { statement SEP } EOF .
block = "{" { statement SEP } "}" .
statement = let-stmt | assign-stmt | multi-assign | expr-stmt | return-stmt
| if-stmt | while-stmt | for-stmt | try-stmt | throw-stmt
| break-stmt | continue-stmt | defer-stmt | assert-stmt
| import-stmt | from-stmt | export-stmt
| fn-decl | class-decl | enum-decl .
(* declarations *)
let-stmt = ( "let" | "const" ) binding "=" expression .
binding = identifier [ type-ann ]
| identifier "," destructure { "," destructure } (* let a, b = f() *)
| list-destructure | map-destructure .
destructure = identifier | list-destructure | map-destructure .
list-destructure = "[" [ destructure { "," destructure } ] [ "..." identifier ] [ "," ] "]" .
map-destructure = "{" [ map-entry { "," map-entry } [ "," ] ] "}" .
map-entry = identifier [ ":" destructure ] [ "=" expression ]
| text-literal ":" destructure [ "=" expression ] .
assign-stmt = target assign-op expression .
multi-assign = target "," target { "," target } "=" expression { "," expression } .
target = identifier | postfix "." identifier | postfix "[" expression "]" .
assign-op = "=" | "+=" | "-=" | "*=" | "/=" | "//=" | "%=" | "**="
| "&=" | "|=" | "^=" | "<<=" | ">>=" .
expr-stmt = expression .
return-stmt = "return" [ expression { "," expression } ] . (* only inside fn *)
throw-stmt = "throw" expression .
break-stmt = "break" . (* only inside a loop *)
continue-stmt = "continue" .
defer-stmt = "defer" block . (* only inside fn *)
assert-stmt = "assert" expression [ "," expression ] .
import-stmt = "import" ( identifier | text-literal ) [ "as" identifier ] .
from-stmt = "from" ( identifier | text-literal ) "import" import-name { "," import-name } .
import-name = identifier [ "as" identifier ] .
export-stmt = "export" ( fn-decl | class-decl | enum-decl | let-stmt ) . (* top level, one name *)
(* control flow *)
if-stmt = "if" expression block { "else" "if" expression block } [ "else" block ] .
while-stmt = "while" expression block .
for-stmt = "for" for-target { "," for-target } "in" expression block .
for-target = identifier | list-destructure | map-destructure .
try-stmt = "try" block { catch-clause } [ "finally" block ] . (* at least one of them *)
catch-clause = "catch" [ identifier [ ":" type-ref { "or" type-ref } ] ] block .
type-ref = identifier { "." identifier } .
(* functions *)
fn-decl = [ "async" ] "fn" identifier params [ "->" type ] block .
params = "(" [ param { "," param } [ "," ] ] ")" .
param = ( identifier | list-destructure | map-destructure ) [ type-ann ] [ "=" expression ]
| "..." identifier
| "**" identifier . (* last *)
(* classes and enums *)
class-decl = "class" identifier [ "extends" type-ref ] "{" { class-member [ ";" ] } "}" .
class-member = [ "static" ] ( "let" | "const" ) identifier [ type-ann ] [ "=" expression ]
| [ "static" ] [ "async" ] "fn" identifier params [ "->" type ] block .
enum-decl = "enum" identifier "{" identifier { "," identifier } [ "," ] "}" .
(* types: parsed and kept on the nodes, ignored at run time *)
type-ann = ":" type .
type = single-type { "|" single-type } .
single-type = type-ref [ "<" type { "," type } ">" ]
| "fn" "(" [ type { "," type } ] ")" [ "->" type ]
| "(" type ")" | "nil" | "class" .
(* expressions, lowest precedence first *)
expression = "if" expression "then" expression "else" expression
| "yield" [ expression ] (* only inside fn *)
| or-expr .
or-expr = and-expr { "or" and-expr } .
and-expr = not-expr { "and" not-expr } .
not-expr = "not" not-expr | equality .
equality = comparison { ( "==" | "!=" ) comparison } .
comparison = range-expr { comp-op range-expr | ( "is" | "is" "not" ) type-ref } .
comp-op = "<" | "<=" | ">" | ">=" | "in" | "not" "in" .
range-expr = bit-or [ ( ".." | "..=" ) bit-or ] .
bit-or = bit-xor { "|" bit-xor } .
bit-xor = bit-and { "^" bit-and } .
bit-and = shift { "&" shift } .
shift = additive { ( "<<" | ">>" ) additive } .
additive = term { ( "+" | "-" ) term } .
term = unary { ( "*" | "/" | "//" | "%" ) unary } .
unary = ( "-" | "~" | "!" | "not" | "await" ) unary | power .
power = postfix [ "**" unary ] . (* right-associative *)
postfix = primary { call | index | slice | member } .
call = "(" [ argument { "," argument } [ "," ] ] ")" . (* "(" on the same line *)
argument = expression | "..." expression | identifier "=" expression .
index = "[" expression "]" . (* "[" on the same line *)
slice = "[" [ expression ] ":" [ expression ] [ ":" [ expression ] ] "]" .
member = "." ( identifier | keyword ) .
primary = number | text-literal | "true" | "false" | "nil" | identifier
| "self" | "super" "." identifier
| list-literal | map-literal | fn-expr | lambda | match-expr
| "(" expression ")" .
list-literal = "[" [ item { "," item } [ "," ] ] "]" .
item = expression | "..." expression .
map-literal = "{" [ map-item { "," map-item } [ "," ] ] "}" .
map-item = expression ":" expression | "..." expression .
fn-expr = [ "async" ] "fn" params [ "->" type ] block .
lambda = ( identifier | params ) "=>" ( expression | block ) .
(* match *)
match-expr = "match" or-expr "{" { match-arm [ "," ] } "}" .
match-arm = pattern [ "if" expression ] "=>" ( expression | block ) .
pattern = single-pattern { "or" single-pattern } .
single-pattern = literal [ ( ".." | "..=" ) literal ] (* ranges: numbers only *)
| "_" | identifier (* binds *)
| identifier "." identifier { "." identifier } (* a value: Level.low *)
| "[" [ pattern { "," pattern } ] [ "..." [ identifier ] ] [ "," ] "]"
| "{" [ map-pat { "," map-pat } [ "," ] ] "}"
| type-ref "(" [ pattern ] ")" (* "(" on the same line *)
| type-ref "{" [ field-pat { "," field-pat } ] "}" (* "{" on the same line *)
| "is" type-ref
| "(" pattern ")" .
literal = [ "-" ] number | text-literal | "true" | "false" | "nil" .
map-pat = ( text-literal | number | identifier ) [ ":" pattern ] .
field-pat = identifier [ ":" pattern ] .
Ambiguities resolved (settled)
{right after the condition ofif/while, the iterable offor, the subject ofmatch, a parameter list or a class name starts a block. A map literal inside the condition is fine (if m == {"a": 1} { … }); a map as the whole condition needs parentheses.- A statement starting with
{is a map literal (there are no bare blocks). - After
=>(a lambda or a match arm),{starts a block unless it looks like a map:{},{..., or a first key (text, number, name or f-text) followed by:. Writex => ({a + 1: 2})for other maps. x => …is a lambda when a name, or a parenthesised list followed by=>, starts an operand.fn name(…) { }declares;fn(…) { }is an expression.a..b step nis not syntax; use(a..b).step(n).
3. Values and types
type(x) | Values | Notes |
|---|---|---|
"number" | ints (unlimited) and floats (IEEE double) | x.is_int(), x.is_float(), int(x), float(x), x.to_int() |
"text" | immutable Unicode text | t[i], t[a:b], negative indexes |
"bytes" | immutable byte sequences | b"...", "é".bytes(), data[i] is an int |
"bool" | true, false | |
"nil" | nil | |
"list" | ordered, mutable | |
"map" | insertion-ordered, mutable; keys are text or numbers | anything else as a key is an error |
"function" | fns, lambdas, methods, built-ins | .name, .arity, .call(list), .bind(...) |
"class" | classes, including Error and its family | |
| class name | instances: type(Dog("Rex")) is "Dog" | |
"module" | imported libraries and files | |
"range" | a..b, a..=b (lazy) | (settled) |
"generator" | generator calls and lazy iterators ([1].iter()) | (settled) |
"enum" | an enum itself (type(Level)) | |
| enum name | enum values: type(Level.low) is "Level" | (settled) .name, .index |
"done" | the iterator sentinel done | |
"Task" | what calling an async fn gives | §9 |
Truthiness: false and nil are false; everything else (including 0, "", []) is true (as in 2.x).
Equality ==: numbers by value (1 == 1.0), text/bytes by content, lists and maps item by item, ranges by bounds, functions/modules/classes by identity, instances by identity unless the class defines equals(other), enum values by identity. Values of different kinds are never equal (1 == "1" is false; nil == false is false).
Ordering < <= > >= (settled): numbers with numbers, text with text (code-point order), instances through compare(other). Everything else — lists included — is an error ("I can't compare list with list.").
3.1 Numbers
+ - *on two ints give an int;/always divides exactly (7 / 2is3.5);//floor division;%floor modulo (the sign of the divisor:-7 % 3is2,10 % -3is-2);**power (2 ** -1is0.5). Dividing by zero is an error.- Display is tidy: a float with no fractional part prints like an int (
6 / 3shows2), so 2.x programs print the same. - Bitwise
& | ^ ~ << >>need whole numbers. - Comparisons chain:
0 < x <= 10means0 < x and x <= 10(x once). - Guards: float overflow and
**results beyond about 10 000 bits stop with "That number got too big for me to work with." +with text on either side joins as text ("x" + 1is"x1", as in 2.x).
3.2 Text, lists and bytes
Indexing and slicing follow Python's rules: t[0] first, t[-1] last, t[a:b] from a up to (not including) b, t[::2], t[::-1]; an index out of range is an error, a slice out of range is clipped. Text can't be changed in place (s[0] = "x" is an error); lists can. list * n and text * n repeat (n must be 0 or more).
Bytes methods: len() hex() slice(a, b?) to_text(encoding?) at(i) starts_with(b) find(b) split(b) join(list) reverse() to_list() copy(); + concatenates; for byte in data gives ints 0–255; b"b" in b"abc".
4. Expressions and operators
Precedence, tightest first (the grammar in §2 is exact):
- member
., call(), index[], slice **(right-associative; its left side is a postfix expression, so-2 ** 2is-4)- unary
-~!await(andnotinside an operand:a == not b) * / // %+ -<< >>&^|.. ..=(1 + 2..5is3..5) (settled: ranges sit below the bit operators)< <= > >= in not in is is not(chained)== !=notandorif … then … else …,yield, lambdas
and/or short-circuit and give back an operand (nil or "x" is "x", 0 or 1 is 0). not x is a bool. x is T is true when x has the built-in type T (number int float text bool nil list map bytes function class module range generator enum any) or is an instance of class T or a subclass. x in y: an item of a list, a number in a range, a key of a map, a substring of text, a sub-sequence of bytes, or contains(x) on an instance.
Spread: f(...xs) passes the items as arguments; [...a, ...b]; {...m, "k": 1} (later keys win).
if c then a else b evaluates only the chosen branch.
5. Statements
letdeclares in the current scope. Blocks (if,while,for,matcharms,try/catch/finally, function bodies) open scopes; a name declared inside is gone after the block. Declaring a name again in the same scope replaces it (settled: not an error). Assigning to a name that was never declared is an error with a hint ('x' doesn't exist yet. Use 'let x = ...' first.).- Destructuring:
let [a, b, ...rest] = xs;let {name, ip: address, port = 22} = rec; a missing key without a default is an error naming the key; nesting is free. The same patterns work inforand in parameters. let a, b = f()unpacks a list with exactly that many items (more or fewer is an error suggesting...rest).let a, b = 1, 2is not allowed; writelet [a, b] = [1, 2].return a, breturns the list[a, b].a, b = b, aevaluates every value before assigning; targets may be names, fields or list slots.x op= eisx = x op e(the target is evaluated once).const NAME = v(or a destructuring pattern): assigning to it later is an error.for x inlists, maps (keys;for k, v in mgives pairs), text (characters), ranges, bytes (ints), generators and instances withiter().for i, x in xs.enumerate().defer { … }(inside a function) runs when the function returns or throws, last registered first.assert cond/assert cond, "message"throwsAssertionError.returnoutside a function,break/continueoutside a loop anddeferoutside a function are errors when the program is read.
6. Functions
fn greet(name, greeting = "Hello", ...rest, **opts) -> text { … }
- Parameters: positional, with defaults (evaluated at call time; a default may use earlier parameters:
fn add(a, b = a * 2)), one...rest(a list of extra positionals; inputs after it need defaults), and a last**opts(a map of named arguments that match no parameter). Any parameter can be passed by name:greet("Tanisha", greeting = "Hi"). A positional argument after a named one, the same parameter given twice, a missing required argument, or an unknown name (without**opts) is an error. Standard-library (native) functions take named arguments too (TB 3.1): their parameter names are the ones in the doc signature's first line (map(fn, items, workers?)), soparallel.map(f, xs, workers = 8)works, with the same errors. A left-out optional parameter before a named one gets the library's default. Functions taking any number of arguments (join(part, ...)) are positional only. - Lambdas:
x => x * 2,(a, b) => a + b,() => 1, andx => { … }whose value is what an explicitreturngives, else nil (settled). fn(x) { … }andasync fn(x) { … }expressions.- Closures capture variables, not values: a later change is seen.
- Function values:
.name(the declared name;"anonymous"forfnexpressions and lambdas (settled)),.arity(number of positional parameters),.call(list),.bind(...args)(fixes arguments from the left). - Recursion depth is limited; going deeper is a friendly error.
7. Classes, enums and modules
7.1 Classes
class Animal {
let sound = "..." # instance field default (evaluated for each instance)
static let count = 0 # class field: Animal.count
fn init(name) { self.name = name; Animal.count += 1 }
fn speak() { return f"{self.name} says {self.sound}" }
static fn make(name) { return Animal(name) }
fn text() { return f"Animal({self.name})" }
}
class Dog extends Animal {
let sound = "woof"
fn init(name) { super.init(name); self.tricks = [] }
fn speak() { return super.speak() + "!" }
}
selfis implicit in methods.super.method(...)calls the parent's version with the sameself.selfcan't be assigned.- Calling a class makes an instance: field defaults first (parent first), then
initif there is one. Withoutinit, any argument is an error. - Single inheritance. Fields live on the instance;
x.fields()gives a map copy. Reading a missing field or method is an error with "Did you mean". - Special methods:
text()(used bysay,str, f-text),equals(other)(==),compare(other)(negative/zero/positive;<and sorting),get(i)/set(i, v)(indexing),call(...)(calling an instance),iter()/next()(iteration;next()returnsdoneat the end),len(),contains(x)(in), and the operator methodsadd sub mul div floor_div mod pow(andradd…) for+ - * / // % **. type(instance)is the class name;type(Dog)is"class";Dog.nameis"Dog"(settled);d is Animal.- Display without
text():Dog {name: "Rex", tricks: []}. - Private by convention:
_name(no enforcement;tb checkwarns).
7.2 Enums
enum Level { low, medium, high } defines Level.low … Values compare by identity, display as Level.low, have .name and .index, and Level.values() lists them in order. They work in match (as dotted value patterns). They are not map keys (settled: keys stay text or numbers).
7.3 Modules and exports
A .tb file is a module. import "helpers.tb" (relative to the importing file) binds helpers; import fs loads a standard library; import x as y renames. If a module uses export anywhere, only exported names can be used from outside (using another is an error that lists the exported ones); otherwise every top-level name is public. from m import a, b as c binds names directly. A module runs once per program; importing in a circle is an error that shows the circle.
8. Errors
Erroris a built-in class:Error(message, fields?)withmessage,line,file,kind(the class name in snake case:"error","value_error","my_err"forclass MyErr extends Error) andstack(list of{fn, line, file}), plus any fields from the map:throw Error("msg", {"code": 7})thenerr.code.throw "text"throws anError;throw {"message": …, …}copies the map's fields (2.x compatibility).- Built-in subclasses:
TypeError ValueError FileError ParseError NetworkError PermissionError NotFoundError TimeoutError StopIteration AssertionError. Errors made by the language itself (an index out of range, dividing by zero) are plainErrors with kind"error"(settled); libraries use the subclasses. try { } catch err: FileError { } catch err: ParseError or ValueError { } catch err { } finally { }: the first matching clause runs;finallyalways runs (also afterreturn,breakor an uncaught error).- An uncaught error prints
Oops! On line N: <message>(with the file name when it happened in an imported file), plus a short call trace when functions were running. A Python traceback is never shown.
9. Generators, iteration and tasks
- A function containing
yieldreturns a generator when called; its body runs lazily.g.next()gives the next value, thendoneforever.for x in g;.to_list() .map(fn) .filter(fn) .take(n) .zip(other) .enumerate()(all lazy exceptto_list). There is noyield from. list.iter(),map.iter()and ranges are lazy iterables too.- Iteration protocol: a value with
next()returningdoneat the end is an iterator; a value withiter()returning one is iterable. async fn(declaration, method or expression): calling it starts aTaskrunning on another thread and returns it at once.await twaits for the task and gives its value (or rethrows its error);awaiton a plain value gives the value. Anasync fncan'tyield. See theparallellibrary forparallel.run,parallel.all, queues and locks.
10. Patterns (match)
let label = match level {
"critical" or "high" => "act now"
n if n is number and n > 7 => f"score {n}"
[first, ...rest] => f"starts with {first}"
{"kind": "alert", "level": lv} => lv
1..=5 => "low range"
Level.low => "an enum value"
Alert(a) => a.name
is text => "some text"
_ => "unknown"
}
- Arms are tried in order; the first whose pattern matches and whose guard is true wins. Bindings belong to the arm. Arms are separated by newlines (or
,). - Literal patterns compare with
==; negative numbers are allowed. - A bare name always binds the value (settled: even when a
constor a class has that name — useName(...)or a dotted name to compare);_matches without binding. A dotted name (Level.low,config.MAX) is a value compared with==. - List patterns match lists of exactly that length, or of at least the fixed items when
...rest(or a bare...) is present. - Map patterns match maps that have every listed key with a matching value; extra keys are fine.
{name}is shorthand for{"name": name}. There are no defaults in match map patterns (settled) (destructuringlethas them). - Range patterns are numbers only (settled):
1..5,1..=5,-3..0. Name(p)matches instances of the className(or a subclass) and matchespagainst the instance;Name {field: p, other}matches fields.is Tmatches by type, like theisoperator.orcombines alternatives; an alternative need not bind the same names (settled).- No arm matching → error "Nothing in this match fits <value>. Add a '_ => ...' arm for everything else."
matchis an expression (a block arm's value is its last expression statement, else nil) and can stand as a statement.
11. Formatting
In f-text, {value!r} shows the value as code (text quoted), {value:spec} applies a format spec, and {value!r:>20} does both. The free function format(value, spec) and the method value.format(spec) on numbers and text do the same as a hole (settled).
format-spec = [ [ fill ] align ] [ sign ] [ "0" ] [ width ] [ "," ] [ "." precision ] [ kind ] .
align = "<" | ">" | "^" .
sign = "+" | "-" | " " .
kind = "f" | "d" | "x" | "X" | "b" | "o" | "%" | "e" | "s" .
Examples: {x:.2f} → 3.14, {n:>5}, {n:<5}, {s:^6}, {n:08.3f} → 0007.000, {n:,} → 1,234,567, {n:x} → ff, {p:.1%} → 25.0%, {s:.3} → hel (truncate), {s:*^5} → **s**. Text aligns left by default, numbers right. An unknown spec is a friendly error that names it and lists the common ones.
12. Program structure, imports and packages
How import finds things:
- Quoted names are files relative to the importing file:
import "helpers.tb",from "lib/x.tb" import f. A quoted name with no extension and no folder (import "tb-colors") is a filetb-colors.tbnext to the importer if there is one, else a package; the bound name replaces-with_(tb_colors) (settled). - A bare name is a standard library (
fs,json, …), else a package. - Packages live in
tb_packages/<name>/, found by walking up from the importing file's folder to the nearest folder with atb.toml; the entry islib.tb,<name>.tb, or[package] mainin the package's owntb.toml.tb add,tb remove,tb update,tb packages,tb initandtb publishmanagetb.toml,tb.lockandtb_packages/.
Packages are TB-only (no native code).
A program runs its statements top to bottom. os.args holds its arguments; exit(code) stops it. Output goes through say; input through ask.
13. Compatibility with TB 2.x
Breaking changes (see MIGRATION-3.0.md):
- The new reserved words in §1.3 (
class,match,const,from,is,self,then, …) can't be names any more; the error suggests another. /always divides exactly and gives a float value (display unchanged); use//for whole division.
Everything else from 2.x keeps working: fn(x) { }, say, ask, the map and list methods, import "file.tb", try { } catch err { } (and the statement words used as names), _test.tb tests.
Appendix A — Conformance tests
spec/*_test.tb hold the conformance tests, one file per area: lexical_test.tb, numbers_test.tb, text_test.tb, collections_test.tb, statements_test.tb, functions_test.tb, classes_test.tb, errors_test.tb, generators_test.tb, match_test.tb, format_test.tb and imports_test.tb (with the helper modules in spec/support/). Each check cites the section it covers, e.g. # §3.1 floor modulo. Run them with:
tb test spec
TB_ENGINE=tree tb test spec # the same rules on the tree-walking interpreter