Reference #
Cheatsheet #
| I need to... | Stability | |
|---|---|---|
| Define a new variable | x := 1 |
✔️ Stable |
| Reassign an existing variable | x = 2 |
✔️ Stable |
| Define a function | main() {} |
✔️ Stable |
| Define a struct | Point {} |
✔️ Stable |
| Define input and output types | f(a int) -> (b int) {} |
✔️ Stable |
| Define same function for other types | f(_ string) {} f(_ int) {} |
🚧 Experimental |
| Instantiate a struct | Point{x: 1, y: 2} |
✔️ Stable |
| Allocate a type | new(int) |
🚧 Experimental |
| Allocate an array | new(int, 10) |
🚧 Experimental |
| Allocate a struct | new(Point) |
🚧 Experimental |
| Allocate and initialize a struct | new(Point){x: 1, y: 2} |
🚧 Experimental |
| Delete an object | delete(p) |
✔️ Stable |
| Define a struct method | f(p *Point) {} |
🚧 Experimental |
| Call struct methods | p.f() |
✔️ Stable |
| Access struct fields | p.x |
✔️ Stable |
| Dereference a pointer | [ptr] |
✔️ Stable |
| Index a pointer | ptr[0] |
✔️ Stable |
| Slice a string | "Hello"[1..3] |
✔️ Stable |
| Slice a string from index | "Hello"[1..] |
✔️ Stable |
| Slice a string until index | "Hello"[..3] |
✔️ Stable |
| Return multiple values | return 1, 2 |
✔️ Stable |
| Loop | loop {} |
✔️ Stable |
| Loop 10 times | loop 0..10 {} |
✔️ Stable |
| Loop 10 times with a variable | loop i := 0..10 {} |
✔️ Stable |
| Jump to the next iteration | loop.next() |
🚧 Experimental |
| Jump to the end of the loop | loop.stop() |
🚧 Experimental |
| Branch | if {} else {} |
✔️ Stable |
| Branch multiple times | switch { cond {} _ {} } |
✔️ Stable |
| Define a constant | const { x = 42 } |
✔️ Stable |
| Declare an external function | extern { g { f() } } |
✔️ Stable |
| Output a string | io.write("Hello\n") |
✔️ Stable |
| Output an integer | io.write(42) |
✔️ Stable |
| Cast a type | x as byte |
🚧 Experimental |
| Mark a type as a resource | ! |
🚧 Experimental |
| Mark a parameter as unused | _ |
✔️ Stable |
Tokens #
Source files are preprocessed by the tokenizer which groups the individual bytes into several token types:
Identifiers #
An identifier like x is a non-empty sequence of letters, digits, and underscores (_).
The first character of an identifier must not be a digit. Identifiers are case-sensitive.
Numbers #
A number like 42 is a non-empty sequence of digits. It may start with a - to indicate negative values. Numbers are decimal by default but the base can be overridden with a 0x prefix for hexadecimal, 0o for octal and 0b for binary. The uppercase letters from A to F are used to represent digits from 10 to 15 in hexadecimal.
Runes #
A rune literal like '日' or '本' is an integer representing a Unicode code point. It must be enclosed by '. It is equivalent to an integer from the perspective of the compiler. The value of the integer is derived from the Unicode representation of the content.
assert 'A' == 0x41
assert 'a' == 0x61
assert '世' == 0x4E16
assert '界' == 0x754C
assert '😀' == 0x1F600
Strings #
A string literal like "Hello" is a sequence of bytes enclosed by ". Strings are immutable, though the compiler does not enforce this rule in its present state. The following escape sequences starting with \ can be used in rune and string literals to embed special characters:
assert '\0' == 0
assert '\t' == 9
assert '\n' == 10
assert '\r' == 13
assert '\"' == 34
assert '\'' == 39
assert '\\' == 92
Comments #
A line comment like // This is a comment starts with // and stops at the end of the line. Comments are ignored by the compiler and can be added to the code for documentation purposes. Multiline comments are not supported.
Operators #
Operators like + represent binary or unary operations.
Operator precedence defines the order of operations. An operation with a higher precedence is performed before operations with lower precedence. Precedence levels introduce additional rules that programmers must learn and can lead to hidden mistakes. To minimize this complexity, Q is limiting the operators to only 8 precedence levels:
| Precedence | Operators | Description |
|---|---|---|
| 8 | . () [] {} |
Postfix |
| 7 | ! - |
Unary |
| 6 | * / % |
Multiplicative |
| 5 | + - & | ^ << >> as |
Additive, bitwise, cast |
| 4 | == != < > <= >= |
Comparison |
| 3 | && || |
Logical |
| 2 | .. , |
Range, separator |
| 1 | := = += -= *= /= %= &= |= ^= <<= >>= : |
Assignment |
Keywords #
| Keyword | Description | Stability |
|---|---|---|
assert |
Tests conditions at runtime | ✔️ Stable |
const |
Defines constant expressions | ✔️ Stable |
else |
Failure branch for if statements | ✔️ Stable |
extern |
Foreign function definitions | ✔️ Stable |
global |
Global variables (discouraged but required in stdlib) | ✔️ Stable |
go |
Asynchronous function calls | 🚧 Experimental |
if |
Branches based on a condition | ✔️ Stable |
import |
Allows access to other packages | ✔️ Stable |
local |
Thread-local variables | 🚧 Experimental |
loop |
Repeatable code | ✔️ Stable |
return |
Ends the function and returns values to the caller | ✔️ Stable |
switch |
Multiple branches executing the first true condition block | ✔️ Stable |
Builtins #
| Function | Description | Stability |
|---|---|---|
cas |
Atomic compare and swap | 🚧 Experimental |
delete |
Frees memory | ✔️ Stable |
new |
Allocates memory | 🚧 Experimental |
syscall |
Calls a kernel function | ✔️ Stable |
Types #
| Type | Description |
|---|---|
int8 |
8-bit signed integer |
int16 |
16-bit signed integer |
int32 |
32-bit signed integer |
int64 |
64-bit signed integer |
uint8 |
8-bit unsigned integer |
uint16 |
16-bit unsigned integer |
uint32 |
32-bit unsigned integer |
uint64 |
64-bit unsigned integer |
int |
Signed integer (native size) |
uint |
Unsigned integer (native size) |
byte |
Alias of uint8 (non-scalar use) |
string |
Alias of []byte (slice of bytes) |
Packages #
A package is defined by a directory. All files in that directory belong to the same package and share access to its identifiers. Subdirectories form separate packages.
Within each package, a function named init is executed automatically at program startup,
while a function named exit runs before the program terminates.
This feature is intended primarily for the standard library and is generally discouraged in application code.
Library #
| Function | Description |
|---|---|
bits.rotateLeft |
Rotate bits left |
c.length |
Compute the length of a 0-terminated string (C style) |
c.string |
Create a 0-terminated string (C style) |
cli.args |
Slice of arguments passed to the program (excluding executable path) |
cli.env |
Look up environment variables |
cli.isTerminal |
Check if the file handle is a terminal |
fs.readFile |
Read a file |
fs.writeFile |
Write a file |
io.read |
Read from stdin |
io.readFrom |
Read from a file handle |
io.write |
Write to stdout |
io.writeTo |
Write to a file handle |
math.newRandom |
Create a new random number generator |
math.sqrt |
Calculate the square root |
mem.alloc |
Allocate a slice of bytes |
mem.copy |
Copy a slice of bytes |
mem.free |
Free a slice of bytes |
mem.zero |
Zero a slice of bytes |
process.id |
Return the process identifier |
process.run |
Runs the given command as a new process |
run.exit |
Exit with an exit code |
strings.cut |
Cut a string in two at a separator |
strings.fromInt |
Convert an integer to a string |
strings.index |
Find a substring |
strings.toInt |
Convert a string to an integer (base 10) |
strings.trim |
Trim whitespace left and right |
strings.trimLeft |
Trim whitespace left |
strings.trimRight |
Trim whitespace right |
thread.id |
Return the thread identifier |
time.now |
Read the current timestamp |
time.since |
Calculate the time passed |
time.sleep |
Sleep the current thread for the given amount of time |
Resources #
Resources are shared objects such as files, memory or network sockets. The use of resource types prevents the following problems:
- Resource leaks (forgetting to free a resource)
- Use-after-free (using a resource after it was freed)
- Double-free (freeing a resource twice)
Any type, even integers, can be turned into a resource by prefixing the type with !. For example, consider these minimal functions:
alloc() -> !int { return 1 }
use(_ int) {}
free(_ !int) {}
With this, forgetting to call free becomes impossible:
x := alloc()
use(x)
x := alloc()
┬
╰─ Resource of type '!int' not consumed
Attempting a use-after-free is also rejected:
x := alloc()
free(x)
use(x)
use(x)
┬
╰─ Unknown identifier 'x'
Likewise, a double-free is disallowed:
x := alloc()
free(x)
free(x)
free(x)
free(x)
┬
╰─ Unknown identifier 'x'
The compiler only accepts the correct usage order:
x := alloc()
use(x)
free(x)
The ! prefix marks a type to be consumed exactly once. It has no runtime overhead. When a !int is passed to another !int, the original variable is invalidated in subsequent code. As an exception, converting !int to int bypasses this rule, allowing multiple uses.
The standard library currently makes use of this feature in two packages:
fs.openmust be followed byfs.closemem.allocmust be followed bymem.free
For memory allocations of slices and pointers delete is called automatically on all exit points of the identifier's scope.
Non-pointer types like !int currently do not support automatic life cycle management and require an explicit free call, e.g. an fs.close for !int file handles.
Errors #
Any function can define an error type return value at the end:
a, b, err := canFail()
An error value protects all the return values to the left of it.
The protected values a and b can not be accessed without checking err first.
Additionally, error variables like err are invalidated after the branch that checked them.
a, b, err := canFail()
// ❌ a and b are inaccessible
// ✅ err is accessible
if err != 0 {
return
}
// ✅ a and b are accessible
// ❌ err is no longer accessible
The error type is currently defined to be an integer, though this is expected to change in a future version.
Security #
Recent incidents such as the xz backdoor and attacks on the npm ecosystem have shown that supply chain attacks remain one of the biggest problems in the software industry.
The build system helps mitigate these risks by enforcing the principle of least privilege. Every module must explicitly declare which resources it requires, such as network or file system access. Any permission change becomes part of the review process during updates, making supply chain attacks much more visible. If leftpad suddenly requests access to net, that should immediately raise suspicion.
While this cannot eliminate supply chain attacks entirely, it significantly reduces the chances of being compromised.
The compiler also hardens executables at the binary level:
- All executables are built as position-independent executables (PIE) with dynamic base addresses so that an attacker can't use precalculated addresses.
- The call stack where return addresses are located is isolated from the regular memory stack, eliminating an entire class of control-flow attacks.
- The W^X (write xor execute) policy is enforced for all memory pages: memory can be writable or executable, but never both.
Syntax #
Q encourages code editors to implement multiple syntaxes for editing.
A view of the code can be substantially different from the underlying model that is saved to disk. It's important to conceptually realize that one is just a temporary view for editing and the other is a form of persistent data storage.
It is absolutely possible that an editor could offer editing in a Python-like whitespace-significant view. It is also possible to offer visual editing with a node-based system similar to Scratch or Unreal Engine blueprints. In all cases the code that is saved to disk would still use the standard text-based format.