Skip to main content

Language reference

Basic Scripting Patterns

Whitespace

In tq, padding can be done with spaces ( ) or tabs (\t). However, we want to ensure a standard pattern of use tabs instead of spaces as the use of the tab character is aways better for cases of acessibility, slightly reduction in source code size and user size preference.


Statements and Line Breaks

Statements are syntatic units that defines actions for the program to take. Statements can be direct instructins like return or statement-like expressions as function calls and conditionals.

Inside a procedure, every statement should be executed one after another in the specified order. To ensire this order is important to know where a statement beggins and ends.

In tq, every line inside a procedure is interpreted as a sinlge statement. e.g.:

Std.Console.writeln("First statement")
Std.Console.writeln("Second statement")

When a line breaks, it means the end of this statement and the beggin of another one. However, expressions are allowed to not follow the same logic. If done right, expressions can be used to break a line without break the logic. Binary operators are a example. When a line breaks before or after it, the compiler will ignore this break. e.g.:

Std.Console.writeln("This string"
+ " is being used to break this line!")

# Is interpreted as
Std.Console.writeln("This string" + " is being used to break this line!")

It is also possible to write two or more statements in the same line using a semicolon (;) character. It will emulate a line break when interpreted. e.g:

write("Hello, "); write("World!"); write("\n")

Comments

Comments are a special syntax resource that allows a section in the script to not be included as executeable code.
Most of it uses includes documentation or fast activation/deactivation of lines or blocks of code.

Single line comments

The # character can be used to declare the section of a line on its right as a comment.

# I am a comment!
IAm.executeable() # Me too!

Multi-line comments

Enclose a section or chunk of lines with ### to declare it as a multi-lined comment.

IAm.executeable()
### I am a comment! Me too! ###

Expressions

Expressions are textual rrepresentations of values or operations.

They may represent:

  • literals
  • variable references
  • operations
  • function calls
  • object access
  • collections
let global = ...

func ... {
Std.Console.writeln( # Function call xpression
gloal # '- Fieeld reference expression
)
Std.Console.writeln( # Fnction call expression
"Hello, World!" # '- String literal expression
)

# Local variables are expressions too!
let collection = {5, 10, 20}.repeat(10 * 10)
# collection ^^^^^^^^^^^
# access ^^^^^^^^^^^^^^^^^^
# Number literal ^^ ^^
# Operation ^^^^^^^
# function call ^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

Expressions can be combined using operators to form larger expressions.

10 + 5 * 20

In this example: 5 * 20 is evaluated first, resulting in 100 then 10 + 100 is evaluated, resulting in 110

because expressions follow precedence and associativity rules similar to mathematics.

Std.Console.writeln(10 + 5 * 20) # 110
# Parenthesis xan be used to explicitly control the evaluation order
Std.Console.writeln((10 + 5) * 20) # 300

Operators

(a, b and c values are expression placeholders)

OperationDescription
a = bAssign the value of b to a
a += bChecked dds the value of a to the value of b, then assigns the result to a
a -= bChecked subtracts the value of a to the value of b , then assigns the result to a
a *= bChecked multiplies the value of a to the value of b, then assigns the result to a
a /= bDivide towards 0 the value of a to the value of b, then assigns the result to a
a %= bDivides the value of a to the value of b, then assigns the reminder to b
a ? b : cReturns b if a is true. If not, returns c
a or bBeing a and b both booleans, returns true if any is true
a and bBeing a and b both booleans , teturns true If both are true
a + bChecked add a with b. If the check fails, throws a fatal error
a +% bUnchecked add a with b, allowing the result to overflow
a +| bChecked add a with b. If the check fails, clamps the result
a - bChecked subtract a with b. If the check fails, throws a fatal error
a -% bUnchecked subtract a with b, allowing the result to overflow
a -| bChecked subtract a with b. If the check fails, clamps the result

Operator Precedence

From lowest precedence to highest precedence.

LevelOperators
1= += -= *= /= %=
2?:
3or and
4+ +% +| - -% -|
5* *% *| / /_ /^ % **
6== != === !== < > <= >=
7& ^ << >>
8as
9Prefix: + - * & ~ ! ? ++ --
10Postfix: ++ -- ! ?
11a.b a(b) a[b]

Expression Types

SyntaxDescriptionInferred Type
123Integer literalint
10.5Floating-point literalf64
"text"String literalstring
'c'Character literalchar
true, falseBoolean literalbool
nullNull literalnull
identifierIdentifier reference(Depends on identifier's type)
.memberImplicit member access(Depends on member's type)
accessed.memberMember access(Depends on member's type)
fun()Function or method call(Depends on fun's return type)
arr[index]Index access(Depends on arr's element type)
(expr)Parenthesized expression(Depends on expr's type)
a + bBinary expression
-xPrefix unary expressiontypeof(x)
x++Postfix unary expressiontypeof(x)
a ? b : cTernary expression(Same as both b and c)
x as TCast expressionT
throw exprThrow expressionStd.Types.Error
new T(...)Object constructionT
match expr { ... }Match expression(Depends on case results)
{expr1, expr2, ...}Generic collection/type expressionSlice of common type of expr1, expr2, etc.
let T x let xLocal variable declarationT or inferred type
const T x const xLocal constant declarationT or inferred type

Data and Types

tq is a strictly-typed language. It means that every data or object that holds data is fully tied to a single data type that can be explicitly declarated by the user or infered in case of being ommited.

Like in C, C# or Java, all input, output and storage of data needs to be declared with a specific type to make sure any binary data will be read or written accordingly.
Type strictness (AKA static typing) is an important resource in a program language as it can be used to define complex structures, non-numerical data, lists, references and alike.

An example on the use of types:

from Std.Console import

# The function main will ask for a list of strings
# to be called. It also declares a return type
# 'void', meaning it will not return any valid data.
@public func main([]string args) !void {

# Let's declare 3 variable data spaces in program's
# memory, one for a signed byte, short and integer.
let i8 myByte = 8
let i16 myShort = 16
let i32 myInt = 32

# Now, let's pass these variables in a function 'foo'
# which is an overloaded to accept multiple types as
# shown below.

# The first call is receiving an 'i8' as its argument.
# As the function is declared to accept 'i8', this
# code will be executed.
foo(myByte) # foo(i8) -> void

# The second call is receiving a 'i32' as its argument.
# The function has an overload that accepts a 'i32'.
# Hence, this code will be executed.
foo(myInt) # foo(i32) -> void

# The third call is receiving an 'i16' as its argument.
# It can be seen below, that we don't have any overload
# of 'foo' that accepts a value of the type 'i16'.
# However, as the type 'i16' is implicitly convertible
# to the type 'i32', the program will implicitly
# convert the argument to i32 and call the right
# overload.
foo(myShort) # foo(i32) -> void

}

# Overloads of the function 'foo'
@public func foo(i8 value) {
writeln("The value is a byte and it is \{value}!")
}
@public func foo(i32 value) {
writeln("The value is an int32 and it is \{value}!")
}
Console Output
The value is a byte and it is 8!
The value is an int32 and it is 32!
The value is an int32 and it is 16!

Bellow, there is a list o the language's built-in types.


Integer

The most simple way of representing numbers is with integers. Integers represents an always complete (non-fractional) value, never allowing any decimal point.

For historical, computational and memory optimal reasons, the computer gives an extensive range of possibilities for numeric integer types.

To declare an integer type in the tq Language, use the low-case letter i or u to declare the kind of the integer (signed or unsigned respectively) and a number to declare the size, in bits, that this data will occupy in memory.

Examples of valid integer types are:

i8   # signed 8-bit (1 byte) integer
u16 # unsigned 16-bit (2 bytes) integer
i128 # signed 128-bit (16 bytes)integer
u256 # unsigned 256-bit (32 bytes) integer
i1 # signed single bit (0 or -1)

The size in bits must be a integer inside the range of 1 to 256.

Although,

it is reccomended to avoid bit lengths that are not a power of two greater or equal than 8 (8, 16, 32, 64, 128, etc.). Most real machines will peform calculations faster in this range as the effort to convert the value from and to the defined alignment is lower.
More about it will be covered in alignment.

The following non-exaustive table shows some common integer types and its corresponding type in the C programming language:

AliasEquivalent in CSizeRange
i8int8_t8-bits-128 to 127
i16int16_t16-bits-32,768 to 32,767
i32int32_t32-bits-2,147,483,648 to 2,147,483,647
i64int64_t64-bits-9.2x10¹⁸ to 9.2x10¹⁸
intsize_t(target word size)(target word range)
iptrintptr_t(target pointer size)(target pointer range)
u8uint8_t8-bits0 to 255
u16uint16_t16-bits0 to 65,535
u32uint32_t32-bits0 to 4,294,967,295
u64uint64_t64-bits0 to 1.8x10¹⁹
uintusize_t(target word size)(target word range)
uptruintptr_t(target pointer size)(target pointer range)
bytechar(target byte size)(target byte range)

The int, uint, iptr and uptr types are special integers that are defined by the target. int and uint are the size of the machine's word (aka the integer size that the CPU can handle naturally) and iptr and uptr are the minimum size needed to store the numeric representation of any pointer, being equal or diferent of int/uint depending on the architecture.

E.g.: If compiling the project to an x86 architecture based machine:
int = i32
uptr = u32

If compiling the project to an amd64 architecture based machine:
int = i64
uptr = u64

If compiling the project to a dotnet assemlby:
int = i32
uptr = [System.Runtime]System.UIntPtr


Floating-point

FP(floating-point) numbers are somewhat complicated numerical values used to reproduce fractions. All the standard FP numbers are implemented following the IEEE 754 standard.

Here is the exaustive list of floating-point numeric types with their corresponding type in the C programming language:

AliasEquivalent in C
f16_Float16
f32float
f64double
f80long double
f128_Float128

Boolean

Booleans are types that can only represent the binary values true or false. These two values are extremely important to handle conditionals or hold simple states without messing the values.

bool    # 1-bit | `true` or `false`
let cake = false

if (cake) Std.Console.writeln("Yummy!")
else Std.Console.writeln("The cake is a lie!")
Console Output
The cake is a lie!

String

A string is a data structure that is able to store text data. A string can be used to store and reproduce text characters easily. \

string    # variable size (can be heap or statically allocated)
let string mySpeek = "Hello, World!"
Std.Console.writeln(mySpeek)

mySpeek = "Goodbie, World!"
Std.Console.writeln(mySpeek)
Console Output
Hello, World!
Goodbie, World!

In tq, all strings are by default encoded in the host's default encoding (UTF-8 if no default specified). To be able to use any specific encodings, use the Std.memory.StringBuffer type.


Char

Chars are data structures made to hold a single text character. Char values can either be set or assigned manually with a character value or it can be set by getting a character from an index of a string.
Every character have the same length as a i32 in memory, being able to hold every possible codepoint of the Unicode char set.

note

Keep in mind that some unicode characters may use more than one real character to be stored, e.g. "🇧🇷" = '🇧' + '🇷'.
The Char struct cannot store that characters, use a String or a slice of Chars to fo so!

Chars are explicitly castable from integers lower than 32 bits e.g. u8, i7 or u32 and explicitly castable to any integer.

char    # 32-bits | can represent single unicode codepoints
const string myString = "Hello, World!"

let char myChar = 'U'
myChar = myString[7]
Std.Console.writeln(myChar as byte) # '87'

Range

Ranges are types with a special syntax and are used widelly though the language. They can be representd as:

<start>..<finish>:<step>

And some values can be implicited, like:

..10     # from 0 to 10
..10:8 # from 0 to 10, in steps of 8
5..* # from 5 to undefined limit
5..*:14 # from 0 to undefined limit, in steps of 14

To iterate though a range, get a instance of the range iterator:

from Std.Console import

const myRange = 0 .. 20 : 2
var iterator = myRange.iterator()

var uint i
while (iterator.reachEnd()) : (i = iterator.next()) {
writeln(i)
}

or using a for-loop:

var uint i
for i in iterator {
writeln(i)
}

Void

Void is an abstract type that is used to indicate that a value is not returned.

void

Opaque

Opaque is an abstract type used to represent an unknown-sized data. It can only be used alongside with pointers to represent a runtime data with no specific type.

Semantically, it is equivalent as void in C.

opaque

NoReturn

NoReturn is an abstract type that is used to indicate that a function does not returns.

noreturn

Label

The label do represents the type of a Label.

label

Collections

Collections are extremelly important components of a program for easy and efficient handling of huge amounts of data.
In some examples of data collections are Arrays, Linked Lists, Queues, Stacks and Dictionaries, all those beung built-in or easy to use when using the Std Lib.

info

Keep in mind that different collection structures will have it own pros an cons. Examine with attention each one to understand exactly what is better for your needs.

Arrays

Arrays are fixed-size collections that store multiple values of the same type in contiguous memory.

An array owns its data, meaning all elements are stored directly inside the array itself. The array size is part of its type and cannot change after declaration.

#/// func scope ///

let [5]i32 numbers = {10, 20, 30, 40, 50}

const [_]string fruits = {
"orange",
"mango",
"strawberry",
"tomato"
}

const [20]i8 myBytes

In the example above: [5]i32 declares an array containing 5 elements of type i32
[_]string declares an array containing string values. It's length is inferred from the collection expression
[20]i8 declares an array containing 20 bytes

Because arrays own their memory, copying an array also copies all of its elements.

Slices

Slices are lightweight references to contiguous portions of arrays. Unlike arrays, slices do not own their data. Instead, a slice provides a view into existing array memory.

A slice can reference an entire array or only a portion of it.

Because slices reference the original array memory, modifications made through a slice also affect the underlying array.


let [5]i32 numbers = {10, 20, 30, 40, 50}

let []i32 middle = numbers[1..4]

middle[0] = 99

Std.Console.writeln(numbers)
# out: [10, 99, 30, 40, 50]

the example above:

  • numbers[1..4] creates a slice referencing the elements from index 1 up to 4
  • middle does not store copied values
  • changing middle[0] also changes the respective value in the original array

Slices are commonly used to pass arrays efficiently without copying their contents.

The index Operator

The index operator [i] accesses individual elements inside arrays and slices. The index is written inside square brackets after the array or slice identifier. Indexes start at zero, where 0 represents the first element. The operator returns the element stored at the specified position.


let []i32 myArray = &{10, 20, 30, 40, 50}

let i32 elementAt1 = myArray[1] # = 20
let i32 elementAt2 = myArray[2] # = 30

Variables can also be used as indexes. Any integer numeric type is valid as an index value.


let []i32 myArray = &{10, 20, 30, 40, 50}

let i8 myIndex = 0
Std.Console.writeln(myArray[myIndex])
# out: 10

myIndex = 4
Std.Console.writeln(myArray[myIndex])
# out: 50

Dynamic Arrays

This feature is still not implemented!

Dynamic arrays are wrappers around built-in arrays that automatize the process of resizingit during runtime. Dynamic arrays will require an allocator to work as they lies entirelly on the heap.

from Std.Console import
from Std.Types.Collections import { DynamicArray }

func main() !void {
const byteList = new DynamicArray(i8)()

# Elements can be added to the list with
# no worries as it will dinamically allocate
# memory when needed.
byteList.add(10)
byteList.add(20)
byteList.add(30)
byteList.add(40)

# Elements can also be removed from the list
byteList.removeAt(2)

for i in byteList do writeln(i)
}
Console Output
10
20
40
Pros and cons

The dynamic array is optimized for reading before resizing. It means that it use should be prioritized for cases where the data need to be fastly reed but the same speed is not needed to manipulate the elements.

Queues

This feature is still not implemented!

Queues are DynamicArray's with specific behavior specialized to work as a FIFO list.

from Std.Console import
from Std.Types.Collections import { Queue }

func main() !void {
const byteQueue = new Queue(i8)()

byteQueue.enqueue(10)
byteQueue.enqueue(20)
byteQueue.enqueue(30)
byteQueue.enqueue(40)

writeln(byteQueue.dequeue())
writeln(byteQueue.dequeue())
byteQueue.dequeue()
writeln(byteQueue.peek())
}
Console Output
10
30
40

Stacks

This feature is still not implemented!

Stacks are DynamicArray's with specific behavior specialized to work as a FILO list.

from Std.Console import
from Std.Types.Collections import { Stack }

func main() !void {
const byteStack = new Stack(i8)()

byteStack.push(10)
byteStack.push(20)
byteStack.push(30)
byteStack.push(40)

writeln(byteStack.pop())
writeln(byteStack.pop())
byteStack.pop()
writeln(byteStack.peek())
}
Console Output
40
30
10

Dictionaries

This feature is still not implemented!

Dictionaries are data structures that provides an associative container in the shape of key-value pairs.

Dictionary preserves no insertion order unless explicitly stated otherwise.

// TODO


Structures

Structures, also known as complex data types, are type abstractions to manipulate data in expecific ways that the primitive data types do not allow to.

Structures can include a huge ammount of primmitives, other structures and even behaviour, allowing it to be a powerfull and rich tool for data storage, organization and encapsulation.

An example of a structure being declarated and used is:

struct Biography {
@public let u8 myAge
@public let string myName
@public let string myGithub
}

func ... {
# this is creating a new variable of type Biography!
let Biography myBio = new Biography()

myBio.myAge = 17
myBio.myName = "Camila"
myBio.myGithub = "lumi2021"
}

The structure Biography on the example have 3 fields, an integer myAge and two strings myName and myGithub. The resulting size of this structure, in memory, will be the sum of all fields' sizes plus any metadata that the compiler finds necessary to the program's work.

Functions and Methods

Structures can contain member functions that execute within the structure's context. those functions have access to all the structure's members alongside non-private inherited members.

member functions can be implemented as the same as normal functions inside the structure boundaries. Those functions are only accessible though a instance instance of the structure.

It is also possible to implement functions inside the structure context that cannot be called from the instance, needing to be referred using the type name itself, so called static functions. To define a function as static, use the attribute @static.

Constructors

Sometimes, it is usefull to have a single function to abstract complex initialization of data. for instance, take the following structure declaration example:

struct Biography {
@public let string name
@public let string github

@public let u8 birthYear
@public let u8 age
}

To initialize this structure, the following code:

# Declarating the variable and creating the instance...
let Biography myBio = new Biography()
# Defining the initial data ...
myBio.name = "Camila"
myBio.github = "lumi2021"
myBio.age = 17
myBio.birthYear = 07

Or use the inline constructor to do it:

# Declarating the variable, creating the instance
# and defining the initial data ...
let Biography myBio = new Biography() {
myBio.name = "Camila"
myBio.github = "lumi2021"
myBio.age = 17
myBio.birthYear = 07
}

To make life easyer, the language allows to declarate a special function that will be called automatically when the structure is initialized, called constructor. The constructor can accept parameters, read and set variables and run code as any function can do.

A constructor can be declarated as follows:

struct Biography {
...
constructor() {
# This will run when the structure is instantiated!
}
}

As it need some input data, the constructor will ask for some arguments:

struct Biography {
...
constructor(string name, string gh, u8 age) {
# This will run when the structure is instantiated!
}
}
info

When a constructor is declarated on the structure, to instantiate it, will be required to match the parameters of one of the declarated constructors (it includes empty constructors).

after that, we can process the data how we want:

struct Biography {
...
constructor(string name, string gh, string age) {
# `this` is used to get the current instance.
# it should be used in case of naming conflict.
# Oterwise, it's unecessary.
this.name = name
github = gh
this.age = age
birthday = 24 - age
}
}

As default, the constructor will handle the stack allocation of the data being initialized. Sometimes, however, it is necessary to have control of this step manually in case of initializing data in the heap or something more specific. Constructors do not handle it as they only serve to initialize data as value, being necessary to implement a static method that will manually handle the creation and initialization of the data.

from Std.Mem import

struct Biography {
...
@static create(string name, string gh, string age) *Biography {
let newBio = alloc(Biography)

newBio.name = name
newBio.github = gh
newBio.age = age
newBio.birthday = 24 - age

return newBio
}
}

Static

Packets

This section is still under construction!

While a struct may reorganize it layout on memory in unexpected ways, packets are made to be a exact 1:1 map of its implementation to memory.

Packets are designed to not just have total precision on it size in memory as to allow the user to have full controll of the layout of all the bits stored on it.


Implementation and Usage

Packets can be implemented as structs, using the @packed attribute as following:

@packed(8) struct MyDinner {
@public let bool hasFork
@public let bool hasSpoon
@public let bool hasKnife
@public let bool hasPlate
@public let u4 hungerLevel
}

The parameter x in @packed(x) indicates the total size, in bits, of the object in memory. The parameter is, however, optional and will be automatically calculated if not provided. If provided, though, the compiler will emmit a compilation error if the sum of the fields mismatches the provided value.
Inside the packet, you can include primitives and other structures.

Packets are used at the same way as structures:

let MyDinner dinner = new MyDinner() {
hasFork = true
hasSpoon = true
hasKnife = true
hasPlate = false
hungerLevel = 10
}

A constructor can also be implemented to make the process easy.

and you can use the dot operator (.) to acess it fields:

from Std.Console import
func ... {
if (!dinner.hasFork) writeln("Comrade doesn't have a fork!")
if (!dinner.hasSpoon) writeln("Comrade doesn't have a spoon!")
if (!dinner.hasKnife) writeln("Comrade doesn't have a knife!")
if (!dinner.hasPlate) writeln("Comrade doesn't have a plate!")

if (!dinner.hungerLevel > 10) writeln("They're starving!!!!")
elif (!dinner.hungerLevel > 5) writeln("They're hungry!")
}
Console Output
Comrade doesn't have a plate!
They're hungry!

Manually Modifying Memory Layout

Sometimes, is necessary to declarate exactly how the memory is organized inside a package. For this, the standard library defines with the @packed attribute, the attributes @off, @lay and @pad (abreviations for offset, layout and padding, respectivelly).

They're used to define the positions and sizes of the next fields inside the structure. \

  • @off(x) defines the position of the field based on the origin of x bits;
  • @lay(r) defines the range r of the value, in bits, based on the origin;
  • @pad(r) defines a relative padding left and right based on the range r.

As instance, let's suppose we have the following structure layout for a page of a table:

bytes:0..445..1010..1515..32
data:tag (enum)active (bool)reserved (void)name (5 * u8)index (u16)

As the data follows a structure with specific defined fields, plus there are some values that are not 8-bit alligned, packets should be used to represent this table. A representation of this table should be:

typedef(u32) Tag { ... }

### The range, in bits, are: - tag: 0 .. 32 - active: 32 - reserved: 40 .. 80 - name: 80 .. 120 - index: 120 .. 256 ###
@packed struct TablePage {
@lay(..32) # Define field inside range 0x0 and 0x4
let Tag tag
let bool active # alignment of 1-bits

@off(80) # Define field ofset at 0x0A
let StringBuffer(5) name # alignment of 8-bits * 5
let u16 index # alignment of 16-bits
}

It is also possible to use bytes instead of bits with the range expression's step value. Both @lay and @pad attributes will use the step value as a multiplier. e.g.:

# No need to count the bits, as we are using bytes!

@public @packed struct TablePage {
@lay(..4:8) # Define field inside range 0x0 and 0x4
let Tag tag
let bool active # alignment of 1-bits

@off(10 * 8) # Define field ofset at 0x0A
let StringBuffer(5) name # alignment of 8-bits * 5
let u16 index # alignment of 16-bits
}
Unions

This section is still under construction!

Unions are usefull data structures that stores each one of it fields in the same overlaping memory space. It can be used to diferent purposes, such as storing diferent types in the same space or reinterpreting the same memory as another type.

To define an union, use the @union attribute in any struct.

@union struct ContactMethod {
let PhoneNumber phone
let InstagramUser instagram
let TikTokUser tiktok
}

Unions, by default, are tagged. Access them in an unsafe context for avoiding tag check or use packets to manually define tag-free unions.

OOP

Object Orientation

This section is still under construction!

Object-Oriended Programming in a programming paradigm that uses objects concept to develop programs. Even tq being mainly based on procedural programming paradigm, Object-Orientation can be implemented using structures.

In OOP, a object is refered as a structure that includes fields and precesses, like functions. Also, the object-orientation have 3 important concepts that objects need to be able to follow.


Structures and Classes


Manipulating Structures as References


Structure Inheritance


Structure Encapsulation


Structure Polymorfism


Typedefs

This section is still under construction!

Type definitions, or typedefs, are a powerfull way to declarate primitive-like types with custom rules and behavior.

Declarting Typedefs

To declarate a typedef, you can do:

typedef MyCustomType {
# Here you can define value
# entries and functions
}

To declarate the typedef with a specific undercovered type, use the following:

typedef(MyType) MyCustomType {
# Here you can define value
# entries and functions
}

Using an undercovered type automatically allows you to explicitly cast to that type.

Any type that is not an integer will ask for a compile-time defined value for each value entry.

Defining Entries

There are two kinds of entries that can fit in a typedef: numericals and named entries. Is important to notice that both entries kinds can be used for any type, although they must be defined slightly diferent when using integer types or not. All typedef entries must beguin with the case keyword:

typedef MyCustomType {
case 1, 2, 3, 4, 5
case 10, 15, 20, 25, 30
}

Numeric entries can also be defined using the range notation:

typedef MyCustomType {
case 1..5
case 10..30:5
}

To define named entries, just write it name following the identifiers convention:

typedef MyCustomType {
case 1..5
case 10..30:5

case NamedValue1
case NamedValue2
case NamedValue3
}

Using Typedefs

After define entries inside the typedef, you will be ble to use them as literal values.

When using numeric values, you can just use a number literal included in the typedef:

let MyCustomType = 5

Using a number that is not included in the typedef will result in a compilation error.

When using literal values, you need to first referenceate the typedef or use the dot notation to automatically identify the typedef type, if possible:

let myvar1 = MyCustomType.NamedValue1
let MyCustomType myvar2 = .NamedValue2

Take this typedef implementation as a example:

typedef Food {
case hamburger
case vegan_burger
case x_burger
case salad
case orange_juice
case chocolate_milk
case cola
}

# This function receives a string and
# Normalize the data into `Food`.
# Returns an error if the input is not
# recognized.
func doOrder(string order) !void {

let foodValue = match (order.trim()) {
case "hamburger" => .hamburger
case "vegan burger" => .vegan_burger
case "x burger" => .x_burger
case "salad" => .salad
case "orange juice" => .orange_juice
case "chocolate milk",
"choco milk" => .chocolate_milk
case "cola" => .cola

default => throw falt.UnrecognizedInput()
}

}

References and Pointers

This section is still under construction!


Nullability

This section is still under construction!

In tq, to ensure security, all data and data reference is assumed to exist and be acessable as default . However, very often is usefull to have a way to identify if a data, object or reference exists and is acessible or not.

The language allows to specify when a data can be expected to be nullable or not, also allowing a variety of security features to prevent errors when acessing null values.


The nullable wrapper

It is possible to declarate nullable types wrapping them with the nullable generic type. This wrapping can be made putting the interrogation (?) character right before the desired type.

?i32                # Nullable signed 32-bit integer
?string # Nullable string
?*DynamicArray # Nullable pointer to a dynamic array

Managing wrapped values

The language also provides some features to make the life using nullable types esier.

The nullable wrapper will not allow you to access the data directly.

func foo() ?i32 { ... }

`?i32` is not assignable to `i32`, unwrap the value first!
let i32 myNumber = foo()

Instead, you have two options to acess the internal data:
Checking if the value is not null or ignoring the possibility of a null value.

you can easily check the existence of the value using a conditional statement and use the unrapped value in the condition scope as follows:

func foo() ?i32 { ... }

func ... {
let ?i32 myNumber = foo()
if (myNumber) {
# Inside the if scope, `myNumber` is shadowed
# to unwrap the value
let i32 value = myNumber
}

# Compilation Error! `?i32` is not assignable to `i32`
let i32 value = myNumber
}

Or ignore the check and manually unrap the condition using the .unwrap() call or the ? operator:

func foo() ?i32 { ... }

func ... {
let i32 myNumber = foo()?

You can do as well
let i32 myNumber = foo().unwrap()
}

If you have a default value to use in case of null, you can use the ?? operator to try to unwrap the expression and use the next expression if it's null:

func ?i32 foo() { ... }

func ... {
let i32 value = foo() ?? 0

You can do as well
let i32 value
let ?i32 nullable = foo()
if (nullable) value = nullable
else value = 0
}

Errors

This section is still under construction!

This feature is still not implemented!

All programmers needs to accept that in some moment, their code will, somehow, fail. Failing is normal and acceptable. Sometimes, inevitable.

To make the coding process less stressing, abstrat presents ways to handle errors, solve and clean data and adjust the process routine as the program needs.


Handling Errors

In tq, any error, exception or invalid operation is handled as a Error.
Falts are data sructures that can be raised during runtime to indicate when and where a certain subroutine identified a problem in the execution. It can store data about where the error happened, what happened and aditional data do debug the error as the programmer needs.

func tryToDoSomething() !void { ... }
func tryToReturnSomething() !u32 { ... }

The tryToDoSomething and tryToReturnSomething functions are being typed as !void and !u32, respectively. It means it will return nothing (void) and a integer (u32), with the error wrapper (!) before it, that means that somehow the execution of this subroutine can fail and be aborted.

Aborting a subroutine is not a bad thing when it is handled at the right way. A aborted function execution means no return value, so the caller needs to be notified when it happens and handle a simple alternative routine to allow the execution to continue running without the desired value.

To make sure that the error will not be a problem to the process, the compiler will not allow a failable function to be invoked as usually:

# Try to invoke the function this way will result in a compilation error!
tryToDoSomething()
let u32 foo = tryToReturnSomething()

Instead, you need to unwrap the result or handle the error somehow.

The first way of doing it is, as simply as this is, ignore the error with the .ignored() call or the ! operator after the resulted failable value.

Pay attention that the ignored result will return a nullable value that will be null in case of an error, so if you have sure about the sucess of the operation, you can also unwrap the nullable value:

# This will invoke the function, allowing it to abort without changing the
# current process state.
tryToDoSomething()!
let u32 foo = tryToReturnSomething()!? # nullable unwrapping

You can do as well
tryToDoSomething().ignored()
let ?u32 bar = tryToReturnSomething().ignored() # not unwrapped

If the error cannnot be just ignored and need a specific process branching to handle it, the error wrapper provide the .onCatch() call and the catch operator:

# If the invoke get aborted, the catch block will be executed before
# the execution continue
tryToDoSomething() catch(error err)
Std.Console.writeln("An error of type \{err.name} has occurred!")
tryToReturnSomething() catch {
Std.Console.writeln("Aborting due internal error!")
throw
}

You can do as well
tryToDoSomething().onCatch((error err) => {
Std.Console.writeln("An error of type \{err.name} has occurred!")
})
tryToReturnSomething().onCatch((error err) => {
Std.Console.writeln("The process could not be completed,")
Std.Console.writeln("aborting due internal error!")
throw err
})

And finally, if the error cannot be handled by the current call and is better to be handled by the caller, it's possible to use the try operator.

See that using the try operator will also make the function abort in case of an error.

# If `tryToDoSomething` return a error, the function will abort and
# the error will be raised to the caller
try tryToDoSomething()
let u32 foo = try tryToReturnSomething()

You can do as well
tryToDoSomething() catch throw
let u32 foo = tryToReturnSomething() catch throw

Raising an Error

To allow a function to raise errors and alert any caller about a process error, the function need to be typed with the Error Wrapper Std.Types.Error(type T). To make life easier, the compiler allows a type to be automatically weapped by using tha bang character (!) before the desired to wrap type:

func foo() !void        # A failable function that returns nothing
func foo() !i32 # A failable function that returns na integer
func foo() !f32 # A failable function that returns a floating
func foo() !?bool # A failable function that returns a nullable boolean
func foo() ![]string # A failable function that returns an array of strings

Inside this function, we create a condition to verify the error as follows:

func safeDivide(f32 numerator, f32 denominator) !f32 {
if (denominator == 0) {} # Raise a error here!
return numerator / denominator
}

And we can use the throw statement to raise the exception, passing after it the exception that we want to raise. The error is declarated when it is required to be created and the same name will aways refer to the same error.

info

We strongly reccomend to follow the pattern of error names writing them aways in pascal case (PascalCase) and aways finish the name with an Error sulfix.

func safeDivide(f32 numerator, f32 denominator) !f32 {
if (denominator == 0) {
# Use the `new` operator to create a
# fresh instance
throw new DenominatorCannotBeZeroError()
}

return numerator / denominator
}

Functions

This section is still under construction!

Functions are simple ways to define subroutines or common and repetitive tasks.

The basic concept of a function is a block of code that can maybe accept arguments and can maybe return data. This block can easily be called by other functions in any moment or order.

Declaring and Invoking

A example of function declaration and call in tq is

# Declaring a function called `foo`
func foo() {
Std.Console.writeln("Hello, World!")
}

func main() !void {
Std.Console.writeln("foo will be invoked now...")
# Invoking our foo function
foo()
Std.Console.writeln("foo finished!")
}
Console Output
foo will be invoked now...
Hello, World!
foo finished!

the syntax for declarate a function is:

func <name>(<argument type> <argument name>, <...>) <optional type> { <code to execute> }

where <name> is the identifier for reffer to the function and <optional type> is the type that the function returns, ornothing for void.

A function can be invoked by it reference followed by a opening and closing parenthesis () as follows:

myFunctionReference()

Parameters and arguments

Some functions can ask for arguments to return some operated value or behave diferently depending on how it is needed.

During the function declaration, parameters can be included as typed identifiers to identify their type and allow them to be refered inside the function block.

# the function `foo` will ask for 2 arguments to be invoked.
# - The first can be a value of any type that will be refered as `any`.
# - The seccond is a 32 bit integer that will be refered as `number`.
func foo(anytype any, i32 number) void { ... }

If the function ask for arguments, the values should be declarated, in order, inside the parenthesis

@public func foo(i32 a, i8 b, i128 c) void { ... }

func ... {
const i32 myInt = 100
const myByte = 255
const i128 myLonger = 2 ** 100

# correct call!
foo(myInt, myByte, myLonger)

There's no function 'foo' with this parameter type order!
foo(myByte, myInt, myLonger)

There's no function 'foo' that accepts only a byte!
foo(myByte)
}

Function Overloading

Two or more functions can have the same identifier if they have different types. When a function is declarated with the same name, but with a diferent tipe, it's called overload.

Good Practice

The tq Language emphasizes code readability and understanding over abstraction. As a good practice, make sure to only use function overloading when the result will be the same for the same equivalent values.

Examples
Good Practice
# Different results are named diferently

func writeText(string value) {
Std.Console.writeln("My string is: " + value)
}
func writeText([]char value) {
Std.Console.writeln("My string is: " + string.join(value))
}
func writeNumber(i32 value) {
Std.Console.writeln("My number is: " + value)
}
Bad Practice
# Different results with the same name can result in a
# harder understanding of the code

func writeText(string value) {
Std.Console.writeln("My string is:" + value)
}
func writeText([]char value) {
Std.Console.writeln("My string is:" + string.join(value))
}
func writeText(i32 value) {
Std.Console.writeln("My number is:" + value)
}

Conditionals

This section is still under construction!

Conditionals are a important aspect of a program, allowing it to do specific behaviors with specific inputs.

The language have 4 main kinds of conditional checking: if, elif and else statements and match expressions.

if, elif and else blocks

if, elif and else are the most basic conditional statement.

These statements allows the formation of conditional cascades, checking one condition after another until one of them is true or the cascade ends.

The condition expression evaluated after the if and elif keywords needs to be or result in a Boolean type.

If false, the code will jump the next declarated statement and continue the process in the seccond statement after the condition.

let sayHello = false

if sayHello
Std.Console.writeln("Hello, World!")

# This statement is outside the condition!
Std.Console.writeln("Goodbie, World!")
Console Output
Goodbie, World!

When an elif statement is declarated after a if or elif, the condition will be verified only if the conditions above are evaluated as false.

When an else statement is declarated after a if or elif, the statement will be executed only when all the above conditions are evaluated as false.

As the condition chain is evaluated from top to bottom, sometimes is possible to end up with unreachable conditions.

let i32 value = 10

# First condition evaluated.
# as 10 != 0, it will fall to the next elif
if value == 0
Std.Console.writeln("value is exactly 0!")

# as 10 != 1, it will fall to the next elif
elif value == 1
Std.Console.writeln("value is exactly 1!")

# as 10 > 5, it will fall to the next elif
elif value < 5
Std.Console.writeln("Value is lower than 5 but greater than 1!")

# as 10 == 10, the next statement will be processed
elif value >= 10
Std.Console.writeln("Value is equal or greater than 10!")

# It's impossible to a number to be greater than 11
# and not be greater than 10. This condition is unreachable.
elif value > 11
Std.Console.writeln("Value is greater than 11!")

# A new if keyword will start a new conditional cascade
if value == 11
Std.Console.writeln("Value is exactly 11!")

# If all conditions in the cascade evaluate to false,
# the else statement will aways be processed
else
Std.Console.writeln("Value is not 11")

if more than a statement needs to be executed in case of a condition, it's possible to open a conde block with brackets ({ ... }).

let i32 value = ...

if (value > 30) Std.Console.writeln("Value is greater than 30!")
elif (value < 30) Std.Console.writeln("Value is lesser than 30!")
else {
# Here, this entire code block will be executed in case of the
# value be exactly 30.
Std.Console.writeln("Certainly,")
Std.Console.writeln("the value is")
Std.Console.writeln("exactly 30!")
}

Match

This feature is still not implemented!

Value match expressions are great ways to simplify (and very often, optimize) long if-else blocks. A match expression will test a value against diferent values or conditions and conditionally execute the code of the matched option. If no valid option matches the provided value, it will fall into a default fallback option if provided.

let i32 value = ...

match value {
case 10 => Std.Console.writeln("value is 10!")
case 20 => Std.Console.writeln("value is 20!")
case > 30 => Std.Console.writeln("value is greater than 30!")

default => Std.Console.writeln("Value is neither 10 or 20, but is less or equal to 30!")
}

# Match blocks can return values too!
let i32 by2 = match value {
case 10 => 5
case 20 => 10
case 30 => 15
case 40 => 20

default => value / 2
}

When used as statements and with, match expressions can implement the behavior of directily executing another case block, without testing it's condition. to enable this behavior, use the keyword continue with the block case condition right after it:

typedef Privilege {
case notAllowed
case basic
case moderator
case admin
}

func ... {
let Privilege permissionLevel = ...
let canSee = false
let canWrite = false
let canArchive = false

match permissionLevel {
default => {
Std.Console.writeln("Permission denied!")
Std.Process.exit(1)
}

case .admin => canArchive = true; continue .moderator
case .moderator => canWrite = true; continue .basic
case .basic => csnSee = true
}
Std.Console.writeln("User is \{permissionLevel}")
Std.Console.writeln("Can See: \{canSee}")
Std.Console.writeln("Can Write: \{canWrite}")
Std.Console.writeln("Can Archive: \{canArchive}")
}

Loops

This section is still under construction!

Complex programs aways needs to do repetitive or infinite tasks. Because of it, loops must be used. Loops are structures used to repeat a selected code section until some condition is reached. Bellow are the loop types that the language supports:


While loops

While loops are the most basic loop types. It while it conditional is valid or the code reaches a breakpoint.

These are the possibilities for the while statement syntax:

while <condition> <statement>
while <condition> : <step> <statement>
while <declaration> : <condition> : <step> <statement>

declaration - Context variables that will be accessible inside the loop condition - The boolean value that will be tested before each iteration. The loop will continue running while this is true step - A process to be executed after each loop iteration instruction - what the loop should do every iteration

While loops are the most common way to do repetitive tasks in tq. it behave both like while and for loops in the majority of other languages.

In C
for (int i = 0; i < 5; i++) {
puts("Hello, Scoped World!")
}
puts("Loop break")
In tq
while i = 0 : index < 5 : i++ {
Std.Console.log("Hello, Scoped World!")
}
Std.Console.log("Loop break")
Console Output
Hello, Scoped World!
Hello, Scoped World!
Hello, Scoped World!
Hello, Scoped World!
Hello, Scoped World!
Loop break

Using a break statement, it is possible to stop the loop before its condition fails:

let index = 10
while index >= 0 {
Std.Console.log("Hello, Scoped World!")
index -=-= 2

if index < 5 break
}
Std.Console.writeln(index)
Console Output
Hello, Scoped World!
Hello, Scoped World!
Hello, Scoped World!
4

Using : is also possible to define an extra step for the loop to do after each iteration:

# Parenthesis are optional, but may helps to read!
let index = 0
while (index < 10) : (index++) Std.Console.log("Hello, World!")

For loops

This feature is still not implemented!

from Std.Console import

func ... {
# Looping from 0 to 49
for i in ..50 writeln(i)

# Looping from 0 to 50 in steps of 10
for i in ..50:10 writeln(i)

# Looping though each element of a array
let []byte numbers = {22, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43}
for v in numbers {
write("Example of a prime number:")
writeln(v)
}
}

Labels

This section is still under construction!

The language provides a special syntatic and semantic feature called Labels. // TODO

goto skip
Std.Console.writeln("Line skipped!")

label skip:
Std.Console.writeln("Executing here")

goto statements can be used to manually perform loops

let i = 0

# Implementation of a `do...while` loop

label body:
{
Std.Console.writeln(i)
i++
}
label check:
{
if (i < 50) goto body
}

or manually perform conditional branching


if (choise == 'A') goto if_option_a
else goto if_option_b

label if_option_a:
{
Std.Console.writeln("Option A")
goto end
}
label if_option_b:
{
Std.Console.writeln("Option B")
}
label end:

Std.Console.writeln("End of condition")
warning

Those examples are not real use cases and must not be replicated in real code. Aways prefer the language's built in statements when possible


Namespaces

This section is still under construction!

Namespaces are a resource used for organization and encapsulation, used to divide the project into small blocks and provide an aditional layer of safety when it comes to naming conflicts.

A namespace represents a logical group of individual identifiers, organized in a hieraquical order. Identifiers inside namespaces can be refered relatively to a scope or globally, with fully-qualified identifiers.

imagine the following namespace organization:

namespace Std {
namespace Math {
struct Graph
namespace Vector {
struct Point
}
}
}

Inside thr scope of Graph, it is possible to import the Point struct this way:

Graph.a
from Vector import { Point }

However, in another module, the reference must be fully qualified this way:

main.a
from Std.Math.Vector import { Point }

Structuring Namespaced

In tq, namespace generation is based entirely on directory structure.

Every namespace is genersted based in a directury. Subdirectories generates neasted namespaces.

Files are ignored and their content is put directly inside the parent namespace.

The concept of this system is designed to allow maximum flexibility with namespace design and organization, also helping with organization and auto-documentation.

Let's take the following project folder as a example:

MyProject:
|-- Entities/
| |-- PhysicsBody.tq
| '-- ColisionBox.tq
|-- Envs/
| |-- Earth.tq
| |-- Mars.tq
| '-- Uranus.tq
'-- Math/
|-- Physics.tq
'-- Constants.tq

And knowing that:

  • All the following members are marked with the @public attribute;
  • PhysicsBody.tq, ColisionBox.tq, Earth.tq, Mars.tq and Uranus.tq contais all empty structures that inherits the file name;
  • Physics.tq contains the functions:
    • calculateSpeed(f64 time, f64 force) f64
    • calculateForce(f64 forceX, f64 forceY) f64
  • Constants.tq contains the constants:
    • const f64 earth_gravity = 9.80
    • const f64 mars_gravity = 3.73
    • const f64 uranus_gravity = 8.87

And compiling the module named as MyProject, The compilation will generate the following global reference table for this module:

KIND:   IDENTIFIER:

root MyProject
nmsp MyProject.Entities
struct MyProject.Entities.PhysicsBody
struct MyProject.Entities.ColisionBox
nmsp MyProject.Env
struct MyProject.Env.Earth
struct MyProject.Env.Mars
struct MyProject.Env.Uranus
nmsp MyProject.Math
func MyProject.Math.calculateSpeed
func MyProject.Math.calculateForce
const MyProject.Math.earth_gravity
const MyProject.Math.mars_gravity
const MyProject.Math.uranus_gravity

Naming Limitations

Some file systems, such as unix compliants, allow you to use characters that cannot be used in an identifier. These characters cannot be parsed into identifiers and consequently, cannot be parsed into namespaces. If that's the case, the files and directories are ignored.

By default, the tq build system uses the following regular expressions to search file system entries:
Directories: [a-zA-Z_][a-zA-Z0-9_]
Files: [a-zA-Z_][a-zA-Z0-9_].tq


metaprogramming

Attributes

This section is still under construction!

This feature is still not implemented!

Mostly of those attributes are, yet, not implemented!

Attributes are a metaprogramming resource that allow the user to link generic functions to be executed above program members, allowing them to change the behavior of the program.


Attributes from the Std Library

The language compiler and Starndard Library offers the implementation of some usefefull attributes for generic use. These are all of them:

Access attributes

These attributes controlls the access of the reference of the member.

AttributeUsed inDescription
@publicany program memberAllows any other member to access the reference
@internalany program memberAllows only members of the same project to access the reference
@privateany program memberAllows only parent and sibling members to access the reference
@allowAcessTo(string)any program memberAllows the access of the member to a certain reference
@allowAcessTo([]string)any program memberAllows the access of the member to a certain collection of references
@denyAcessTo(string)any program memberDenies the access of the member to a certain reference
@denyAcessTo([]string)any program memberDenies the access of the member to a certain collection of references
@staticany program memberForces the member to be stored and accessed stactically
@defineGlobal(string)any program memberCreates a named reference for the member in the root of the program
@defineGlobal([]string)any program memberCreates a collection of named references for the member in the root of the program

OOP attributes

These attributes controlls the Object-Oriented behavior of structures.

AttributeUsed inDescription
@finalStructuresDenies inheritance for other structures
@abstractStructuresMarks a structure as abstract
@interfaceStructuresMarks a structure as a interface
@valueOnlyStructuresMarks a structure as never allowed to be allocated in the heap
@referenceOnlyStructuresMarks a structure as never allowed to be allocated in the stack
@virtualFunctions (with structure as parent)Allows a function to be overridden in a structure that inherits it parent
@overrideFunctions (with structure as parent)Overrides a reference to a function of the inherited parent

Function attributes

These attributes modifies how functions are handled by the compiler.

AttributeUsed inDescription
@inlineFunctionsForce a function to be implemented instead of invoked when called
@noInlineFunctionsForce a function to be not inlined during optimization
@comptimeFunctionsForce a function to be executed during compilation
@runtimeFunctionsForce a function to be executed only during runtime
@callConv(Std.Compilation.Abi)FunctionsDeclarate the convention used for the calling on binary targets that support different ABIs

Overloading attributes

Attributes that changes how references behaves when acessed or writed to.

AttributeUsed inDescription
@getter(function)FunctionsCreates or modifies the nammed field in the parent function, using this function and it getter
@setter(function)FunctionsCreates or modifies the nammed field in the parent function, using this function and it setter
@indexerGetFunctionsOverloads the indexer operator for the parent function, using this function and it getter
@indexerSetFunctionsOverloads the indexer operator for the parent function, using this function and it setter
@implicitConvertFunctionsRegisters a implicit conversion of the type in the first parameter of the function to the return type of the function
@explicitConvertFunctionsOverloads the as operator to convert the type in the first parameter of the function to the return type of the function
@overrideOperator(Std.Math.Operator)FunctionsOverloads the action of the specified operator. See Std.Math.Operator
@defineAttributeFunctionsRegister the function as a new attribute with the specified name

Creating your own attribute

This feature is still not implemented!


Importing Modules and Namespaces

Importing allows to use references outside the current script and scope. The Language offers different ways to import diferent references as needed for the user and in a way to allow the readability of the code.


Importing namespaces

simple imports can be done by writing:

from <namespace> import

Being <namespace> the relative or global reference to a namespace that is desired to import.

A example of simple import is:

from Std.Console import

Simple imports will allow the code to directly access members from the imported namespace inside it:

from Std.Console import

func main() !void {
# `writeln` is not a member of the `MyProgram` namespace,
# in reality it is being imported from `Std.Console`!
writeln("Hello, World!")
}

Specifiing Imports

Sometimes, is not necessary to import a entire namespace or doing so can cause a conflict with another member. Because of it, The language allows to specify what is being imported to the current scope with the syntax:

from <namespace> import { <ref...> }

Being <namespace> the parent of the desired imported content and <ref...> the references, separated by comma, of the desired members of the refered namespace.

Doing it also will not only import the desired content, as well scope the content of what is being imported inside a reference of the same name.

e. g.:

from Std import { Console, Mem }
# `Console` and `Mem` are now directly referenceable

func main() !void {
Console.writeln("Hello, World!")
}
from Std.Console import { write }
# only the `write` function is being imported

func main() !void {
write("Hello, World, ")
# Other references still need to be refered
# from the root!
Std.Console.writeln("I'm importing references!")

`writeln` is not defined in the current namespace, neither is being imported from `Std.Console!`
writeln("Oops")
}

Import with alias

To avoid reference conflicts or have control about the imported reference, is still possible to renemed it as desired using the keyword as as follows:

from Std.Console import { write, writeln as writeLine }

func main() !void {
write("Hello, World, ")
# Instead of refering as `writeln`, the function is
# refered as `writeLine`!
writeLine("I'm importing references!")

`writeln` is not defined in the current namespace, neither is being imported from `Std.Console` as `writeln`!
writeln("Oops")
}

Memory

Data References

This section is still under construction!

When it comes to memory management, is important to understand what is data references and dereferencing data before knowing how to use it.


In diverse cases, it's usefull, better for performance or just innavoidable to handle some process with the data source instead of a copy of it. To solve this problem, all systems allows two ways of moving data around:

Sending Data By Value

means that the data being assigned is not the source, but a copy of it. Let's take a example:

const source = 10
const copy1 = source
const copy2 = source

both copy1 and copy2 have the same value stored in source: 10. It doesnt mean that copy1 and copy2 are refering source.

from Std.Console import

const source = 10
const copy1 = source
const copy2 = source

func ... {
writeln("original source = \{source}")
writeln("original copy1 = \{copy1}")
writeln("original copy2 = \{copy2}")

# Modifying...
copy2 = 20
copy1 = 5

writeln("source = \{source}")
writeln("copy1 = \{copy1}")
writeln("copy2 = \{copy2}")
}
Console Output
original source = 10
original copy1 = 10
original copy2 = 10
source = 10
copy1 = 5
copy2 = 20

With this code is possible to see that source, copy1 and copy2 has independent values, though receiving it from the same field reference. This is explained though the fact that the computer makes a copy of the value in source instead of derreferencing the field itself. This way, any modifications on data received by value will not be reflected in the original data source.

Sending Data By Reference

means that the data being assigned is a reference to the source. Follow the example:

const source = 10
const copy1 = source
const ref1 = &source
const ref2 = &source

The unary operator & returns the reference of the field instead of it value. This means that ref1 and ref2 now points to source.

from Std.Console import

const source = 10
const copy1 = source
const ref1 = &source
const ref2 = &source

func ... {
writeln("original source = \{source}")
writeln("original copy1 = \{copy1}")
writeln("original ref1 = \{ref1}")
writeln("original ref2 = \{ref2}")

# Modifying...
ref1 = 5
ref2 = 7

writeln("source = \{source}")
writeln("copy1 = \{copy1}")
writeln("ref1 = \{ref1}")
writeln("ref2 = \{ref2}")
}
Console Output
original source = 10
original copy1 = 10
original ref1 = 10
original ref2 = 10
source = 7
copy1 = 10
ref1 = 7
ref2 = 7

This example shows that assiging a value to ref2 resulted in both source and ref2 being changed to this value too. In reality, only the value of source is changing and it content is being reflected by ref1 and ref2.


Lifetime And Ownership

This section is still under construction!

As part of the language's philosophy, eficiency during memory management is one of our main focus. To help the user to do not get bothered with the tiny details, as default, tq uses a system based in lifetime and ownership to determinate when data explicitly or implicitly allcated on the heap can be safetly and automatically deallocated.

info

Don't confuse "lifetime and ownership" with "ownership and borrowing". The tq Language do not include any kind of borrowing system or borrow checking.


Understanding Lifetime

In a procedural language like tq, every program beggins in a certain defined point (the entry point function) and expands it process in one or multiple linear stacks, usually finishing at the same point that it beggins. The same way as the language knows where a method starts, ends or a data is sent in or out a function, it can understand where these data is being used and where it is being discarted.

let's take the following code as an example:

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
let message2 = "Today is a good day."
sayMessage(message2)
message2 = "How are you going?"
sayMessage(message2)
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
}

With a quick analysis, is not hard to understand when a data reference becomes unaccessible:

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
let message2 = "Today is a good day."
sayMessage(message2)
message2 = "How are you going?"
sayMessage(message2)
# `message1` and `message2` becomes
# out of scope after here
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
# `msg` becomes out of scope
# after here
}

Also it is possible to go more further and identify when a value is no more used or a field has it value changed:

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
# `message1` is not used anymore
let message2 = "Today is a good day."
sayMessage(message2)
# `message2` current value is lost here
message2 = "How are you going?"
sayMessage(message2)
# `message2` is not more used
# and becomes out of scope
# after here
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
# `msg` is not more used
# and becomes out of scope
# after here
}

Knowing where variables are lost or get out of scope, the best practice is aways request the data destruction to make sure that any heap-allocated data from it is well deallocated:

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
destroy message1 # `message1` is not used anymore

let message2 = "Today is a good day."
sayMessage(message2)
destroy message2 # `message2` current value is lost here

message2 = "How are you going?"
sayMessage(message2)
destroy message2

# `message2` is not more used
# and becomes out of scope
# after here
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
# `msg` is not more used
# and becomes out of scope
# after here
}

The Static Garbage Collector

However, tq can ease the duty of cleaning up objects every single time it lifetime ends.

The language included a feature called The Static Garbage Collector, a garbage collector that acts in compile time and is developed to carefully 8nspect, select and delete references when their lifetime ends. Take the following example:

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
# `message1` is not used anymore
let message2 = "Today is a good day."
sayMessage(message2)
# `message2` current value is lost here
message2 = "How are you going?"
sayMessage(message2)
# `message2` is out of scope
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
# `msg` is out of scope
}

Even without any destructor calls, this code can finely be compiled and executed without any leaks, thanks to the static GC.

func main() !void {
const message1 = "Hello, World!"
sayMessage(message1)
# `message1` is not used anymore
destroy message1 # by static GC
let message2 = "Today is a good day."
sayMessage(message2)
# `message2` current value is lost here
destroy message2 # by static GC
message2 = "How are you going?"
sayMessage(message2)
# `message2` is out of scope
destroy message2 # by static GC
}

func sayMessage(string msg) {
Std.Console.writeln(msg)
# `msg` is out of scope
# Static GC does nothing here,
# as `msg` is not only owned
# by this scope.
}

disabling the garbafe Collector

As everthing in tq, you has full controll of every rsource provided. The language gives you full hability to manually choose when resources must be freeled. To disable the garbage collecting analysis and consequently, the implicit reference counting and destroy statements, use the Std.GC namespace to manuually control the garbage collector's behavior:

from MyProgram import { MyHeapObject }
from Std import { GC }

func main() !void {
const string message = "Hello, World!"
Std.Console.writeln(message)
# message is no more used soit is
# destroyed bythe GC

# This will disable the GC untill
# called again
GC.turnOff()

var myObject = new MyHeapObject()

# This enables the GC again
GC.turnOn()

destroy myObject
}

This feature is still not implemented!

as the same way as the language automatically and optionally allocates on heap to make the life easier, it have a simple system of lifetime and ownershiping to know when to try to automatically deallocate these allocations made.

Knowing as every struct can implement a destructor, the language will follow the use of variables inside functions.

When a variable is created inside a function, it data is owned by this function. When a variable is returned by the function, it data is owned by the caller.

When a function returns (dies), all the references that was owned by it will lose the ownership of this function.

When a value do not have any more owners, the destructor of this value type is called.

All this proccess should be handled in compiling thme.

Data inside objects also have a lifetime, but tracked by the ownership of the instance. When the instance dies and it destructor is called, it should also call the destructor of it owned data.

-- lumi


Scope Allocators

This section is still under construction!

Arena allocators with another name lol

Std.Memory should allow to manipulate scope allocators in a stack-based way, where you can just push a new allocator that will be used for everything an then pop it, deallocating everything allocated with it.


Data alignment

This section is still under construction!


Asyncronous

The language's standard library and compiler have built-in features that allows the development of asyncronous programming.

Asyncrony is the hability of schedue calls and routines to run only when they are able to, executing other tasks meanwhile the current one is busy or waiting.

Low-Level

This section is still under construction!

Abstractions are made to make the programmer's life easier, symplifying repedititve or system-dependent tasks.

Sometimes however, it is mandatory to communicate directly with the system. Embedded and bare-metal development, system programming and better memory managing are just some examples of why this access is needed.


Acessing CPU Registers

This feature is still not implemented!

When compiled to some CPUs, the program binary has access to a set of CPU Registers that are used to do operations or controll the CPU behavior.

// TODO


Inline Assembly

This feature is still not implemented!

When read and write to the registers themselves are not enough, tq has a rich system that allow the user to write an entire assembly subroutine.

As a example of use, being targeted to x86_64 assemby:

from Std.Console import
from Std.System.Assembly import { Context as AssemblyContext }

func foo() {

let string message = "Hello, World!"

# Calling writeln(message) though assembly

# This will create the context object.
# The context object is used to store the
# entire assembly code in a tiny scope,
# allowing that code to be emitted as
# is and do not receive any optimizations.
new AssemblyContext(.x86_64)

# The context return an instance that contains
# diverse methods to write the assembly code.
# See the example:

# The first argument must go in the RDI register
# the mov instruction follows the order 'to <- from'
.MOV(.rdi, &message)

# Then we can make a call to the function
.CALL(writeln)

# `.execute()` must be called to the
# code to take effect.
.execute()

#This is equivalent to
### MOV $rax, message CALL Std.Console.writeln ###

}
from Std.Console import
from Std.System.Assembly import { Context as AssemblyContext }

func foo() {

let string message = "Hello, World!"

# Writing to STDOUT in x86_64 linux
new AssemblyContext(.x86_64)

.MOV(.rax, 1) # syscall = write
.MOV(.rdi, 1) # descriptor = stdout
.MOV(.rsi, &message) # message reference
.MOV(.rdx, message.len) # message length
.SYSCALL()

.execute()
}