⚡ vex, a general purpose, highly performant universal code language with an emphasis on code modularity, literacy, readability, maintainability, consistency and developer experience.
- Install
vex
cli - Run
vex new app
You should get a directory like so:
src
main.x
test
benchmark
.keep
e2e
.keep
spec
.keep
package.x
README.md
// main.x
say`Hello, world!`
0. Performance is indisputable.
1.
1.1. Documentation is essential.
1.2. Benchmarking is pivotal.
1.3. Testing is fundamental.
1.4. Readability is crucial.
1.5. Consistency is vital.
2.
2.1. Beautiful is better than ugly.
2.2. Shorter is better than longer.
2.3. Sparse is better than dense.
2.4. Flat is better than nested.
2.5. Nested is better than mess.
3.
3.1 Complex is better than complicated.
3.2 Simple is better than complex.
3.4 Clear is better than ambiguous.
3.3 Explicit is better than implicit.
3.5 Descriptive is better than vague.
4.
4.1. Developer experience over unnecessary struggle.
4.2. Quantity of files over quantity of lines.
4.3. Reusability over repetitiveness.
4.4. Verticality over horizontality.
4.5. Patterns over randomness.
5.
5.1 In the face of ambiguity, refuse the temptation to guess.
5.2 Special cases aren't special enough to break the rules.
5.3 Historical benchmarking over premature optimization.
5.4 Errors should never be logged superficially.
5.5 Exceptions should never pass silently.
The priority for the entrypoint of an application or library goes as follows:
main.x
- An
fn main() -> {}
impure function in any file with this name
- Multiple
main.x
files are prohibited per one single application, library. - Multiple
main
functions are prohibited per one single application, library.
Being a highly literal language it is important to have everything tool and building block to the disposal of the developer, enabling the writing of clear, readable and maintainable code.
// This is an inline comment.
// ! This is a highly important inline comment.
// ? This is an optional, question-like comment.
// @Decorator can be used in an inline comment to enchance it.
/// This is comment that will be available in the generated docs.
/**
* This is a multi-line comment.
*/
/**
* This is a multi-line comment with a @vex/doc decorator.
* @See https://vexlang.com
*/
/**
* This is a chonky multi-line comment with multiple @vex/doc decorators.
* @Author Reanimated Man X <[email protected]> / Alexei Gaidulean
* @See https://vexlang.com
*/
Comment sections are used to group related code together for easier, digestible reading.
Comment sections can be then collapsed/expanded by the IDE, while simple comments can not.
// any.x
# Title commentary
## Sub-section commentary
fn doSomething(): T => {
# Process this thing
// Implementation
# Process that thing
// Implementation
}
# Title commentary 2
## Sub-section commentary 2
fn doSomethingAwesome(): T => {
// Implementation
}
Comment sections are great to vertically split code in meanignfull sections, or "chapters" to list through, however it's not possible to group or encapsulate code in them as all the code behind a section will always be part of it.
To add an encapsulating comment region without adding an extra level of identation:
fn doSomething(): T => {
/# Unspeakable.
say`Anything!`
/#
# Do something else
// Implementation
}
TBD
TBD
vex has all the functionality to print to the terminal using 7
different log levels for general purpose use.
say`Hello, world! 👋`
log`Server started at $address:$port.`
info`Server database seeding succeeded.`
warn`Server database schema is inconsistent.`
alert`Server received unexpected traffic from $ip. Notifying...`
panic`Server database connection failed: $exception. Retrying...`
fatal`Service encountered an unrecoverable error: $error. Exiting...`
dbg(obj)
trace(obj)
time`Long running function`
timeLog`Long running function`
profile`Test case: CPU Spikes`
profileLog`Test case: CPU Spikes`
table([{a: 1}, {a: 2])
group`This is a logging group`
groupEnd`This is a logging group`
vex is structured and build around repetitive, consistent structures called blocks.
Here is a general overview of vex block anatomy.
// Block
👇 Header token
{ 👈 Block header line
👈 Block body line(s)
} 👈 Block footer line
👆 Footer token
There can be 3
types of blocks
// "Encapsulation" block, represented by `{` and `}` characters.
{
// Body
}
// "Enumeration" block, represented by `[` and `]` characters.
[
// Body
]
// "Expression" block, represented by `(` and `)` characters.
(
// Body
)
// Example
bp Blueprint {
// Body
}
en Enum [
A
B
]
compose Compose (
Fn1
Fn2
Fn3
)
// Anonymous block
{
}
// Unique anonymous block
token {
// Body
}
// Unique anonymous block with callable expression
token (expression) {
// Body
}
// Uniquely named block
token Example {
// Body
}
// Uniquely named block with callable expression
token Example(expression) {
// Body
}
// Uniquely named block with callable expression and header extension.
token Example(expression) token value {
// Body
}
// Uniquely named block with callable expression and header and footer extension.
token Example(expression) token value {
// Body
} token value
There are 7
block categories.
- Extension Blocks
- Modeling Blocks
- Building Blocks
- Grouping Blocks
- Storage Blocks
- Logical Blocks
- Loop Blocks
TBD
vex provides access to a wide variety of primitives.
- Bitwise types: Bit
- Signed integers: I8, I16, I32, I64, I128 and ISize (pointer size)
- Unsigned integers: U8, U16, U32, U64, U128 and USize (pointer size)
- Floating point: F32, F64
- Char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)
- Bool either true or false
- The Unit type (), whose only possible value is an empty tuple: ()
ℹ️ Despite the value of a
Unit
type being a tuple, it is not considered a compound type because it does not contain multiple values.
let bitValue: Bit = 1 // 0 or 1
let int8Value: I8
let int16Value: I16
let int32Value: I32
let int64Value: I64
let int128Value: I128
let unsignedInt8Value: uI8
let unsignedInt16Value: uI16
let unsignedInt32Value: uI32
let unsignedInt64Value: uI64
let unsignedInt128Value: uI128
let int8Value: F16 = 1.0 // 16-bit floating point (10-bit mantissa) IEEE-754-2008 binary16
let int8Value: F32 = 1.0 // 32-bit floating point (23-bit mantissa) IEEE-754-2008 binary32
let int8Value: F64 = 1.0 // 64-bit floating point (52-bit mantissa) IEEE-754-2008 binary64
let int8Value: F80 = 1.0 // 80-bit floating point (64-bit mantissa) IEEE-754-2008 80-bit extended precision
let int8Value: F128 = 1.0 // 128-bit floating point (112-bit mantissa) IEEE-754-2008 binary128
Any of the above can be infered via the Number
type
let v = 2 // Implicitly Number
let v = 2: Number // Explicitly Number
// Special
let numberInRange: Number({ range: 1...500 }) = 499
let bFalse: Bool = true
let bFalse: Bool = false
// Char type with backtick delimiters
let charValue: Char = `A`
// String type with backtick delimiters
let stringValue: String = `Hello, world!`
// Symbol type with backtick delimiters
let symbolValue: Symbol = @`Symbol`
let void: Void
let null: Null
let unknown: Unknown
let required: Required
let optional: Optional
let composable: Composable
let returnable: Returnable
let unreturnable: Unreturnable
// Recoverable unexpected behavior
let exception: Exception
// Unrecoverable unexpected behavior
let error: Error
- Arrays
[1, 2, 3]
- Tuples like
(1, true)
Integers 1, floats 1.2, characters 'a', strings "abc", booleans true and the unit type () can be expressed using literals.
Literal modifiers are single characters prefixed, suffixed or part of the literal body.
Below is a list of literal modifiers.
0x
0x80
0o
0b
0b0011
1e6
7.6e-4
100000000 is the same as `10_000_000`
0.0000001 is the same as 0.000_001
true && true
true || false
!true
1 .. 5
TBD Bitwise operators
assert
control block is used for short, non-exhaustive, simple to digest assertions
assert
blocks cannot be nestedassert
blocks do not haveassert if
statements
// Assert guards
assert !isReady {
error`You are not prepared.`
}
// Multiple conditions
// ℹ️ Up to 3 distinct chained conditions in expression
assert isUserInvited || isUserCreated || isUserOnboarded {
say`🍾 Congrats`
}
// Call a function and return the flow to the block parent
assert value == 1 {
doThis()
return
}
// Add an `else` fallback
assert value == 3 {
say`The value is nuts!`
} else {
warn`But something doesn't add up`
}
// Use the expression value locally
assert value == 3 as Some(v) {
say`The value of $v is total nuts!`
} else {
warn`But something doesn't add up`
}
ℹ️ For complex expressions involving many conditions use
match
// Match single value against expressions
match value {
1 -> doThis()
2 -> doThat()
6 || 9 -> doChoose()
3 .. 5 -> doInRange()
_ -> doFallback()
}
// Match all values against expressions
match * {
value1 = 1 -> doThis()
value2 = 2 && isMirror -> doThat()
_ -> doFallback()
}
// Pass the matched value
match value {
1 (v) -> doThis(v)
2 (v) -> doThat(v)
_ (v) -> doFallback(v)
}
// Use block scope for calling multiple impure functions
match value {
1 (v) -> {
callSync(v)
await callAsync(v)
}
2 () -> doThat()
_ () -> doFallback()
}
// Define a matching strategy for functions with identical input parameters
fn do = match {
1 : doThis
2 : doThat
_ : doFallback
}
// Usage
do(value)
let type: Type
TBD
vex
comes with a CLI to do all sorts of things.
xx i
xx add <@scope/package>
xx upd <@scope/package>
xx rm <@scope/package>
Running & Building
xx dev
xx start
xx build
Documenting & Testing
xx docs
xx bench
xx test
Versioning & Distributing
xx tag
xx pack
xx version
xx publish
xx release
vex enforces the user to declare & define a single building block entity per file, unless part of a namespace, therefore learning how to import/export building blocks is an important step moving forward.
1. Use standart sources / packages.
// Use everything.
use fmt
// Use everything at path.
use fmt/deep/internals
// Use everything as a local alias.
use fmt as Alias
// Use only a part.
use fmt:{ Part }
// Use only a part as a local alias.
use fmt:{ Part as Alias }
// Use only a subset.
use fmt:namespace
// Use only a part of a subset.
use fmt:namespace:{ Part }
// Use only a part of a subset as a local alias.
use fmt:namespace:{ Part as Alias }
2. Use internal project sources / packages. (relative)
// Use everything.
use ./relative
// Use everything at path.
use ./relative/deep/path
// Use everything as a local alias.
use ./relative as Alias
// Use only a part.
use ./relative:{ Part }
// Use only a part as a local alias.
use ./relative:{ Part as Alias }
// Use only a subset.
use ./relative:namespace
// Use only a part of a subset.
use ./relative:namespace:{ Part }
// Use only a part of a subset as a local alias.
use ./relative:namespace:{ Part as Alias }
3. Use internal project sources / packages. (absolute)
// Use everything.
use @/src/local
// Use everything at path.
use @/src/local/deep/path
// Use everything as a local alias.
use @/src/local/path as Alias
// Use everything from package manifest.
use @/package
// Use only a part.
use @/src/local/path:{ Part }
// Use only a part as a local alias.
use @/src/local/path:{ Part as Alias }
// Use only a subset.
use @/src/local/path:namespace
// Use only a part of a subset.
use @/src/local/path:namespace:{ Part }
// Use only a part of a subset as a local alias.
use @/src/local/path:namespace:{ Part as Alias }
4. Use external project sources / packages.
ℹ️ All non-standart packages are enforced to be scoped
// Use everything.
use @scope/package
// Use everything at path.
use @scope/package/deep/internals
// Use everything as a local alias.
use @scope/package as Alias
// Use only a part.
use @scope/package:{ Part }
// Use only a part as a local alias.
use @scope/package:{ Part as Alias }
// Use only a subset.
use @scope/package:namespace
// Use only a part of a subset.
use @scope/package:namespace:{ Part }
// Use only a part of a subset as a local alias.
use @scope/package:namespace:{ Part as Alias }
TBD
Namespaces are top-most building block and are used to group related building blocks together.
We can define & concatinate the namespaces as such
// src/user.x
ns User
ns User:Profile // ❌ Compilation error: Another encapsulating namespace block cannot be defined in the same file.
// Using a single `:` token
fn User:myFunction() {
// Implementation
}
en User:UserRoles {
USER
ADMIN
}
Let's analyze this example.
// src/main.x
en UserRoles { // ❌ Compilation Error: Another building block cannot be defined in the same file, unless it's part of an unique namespace.
USER
ADMIN
}
it User { // ❌ Compilation Error: Another building block cannot be defined in the same file, unless it's part of an unique namespace.
name: String
}
fn utilty() => { // ❌ Compilation Error: Another building block cannot be defined in the same file, unless it's part of an unique namespace.
// Implementation exists
}
bp User(&) { // ❌ Compilation Error: Another building block cannot be defined in the same file, unless it's part of an unique namespace.
// Implementation
}
fn main() => {
say`Hello!`
}
To solve it, we extact the blocks into their corresponding files.
// src/util.x
fn utilty() => { // ✅ All good :)
// Implementation exists
}
Then, we create a namespace to group all user related stuff in there.
// src/users.x
ns Users
en Users:UserRoles {
USER
ADMIN
}
bp Users:User(&) { // ✅ All good :)
// Implementation
}
// src/main.x
import { Users } from `./users`
fn main() => {
let user = Users:User()
say`Hello, $user!`
}
All functions receive only one parameter.
This is enforced for various reasons.
- Named Parameters: Using a parameter object allows you to use named parameters, which can make your code more self-explanatory and less error-prone. Instead of relying on the order of parameters, developers can specify the parameter they want to set explicitly.
- Flexibility: With a parameter object, you can easily extend the function's parameters without changing the function signature. This makes it more adaptable to future changes or additional parameters.
- Default Values: You can provide default values for parameters within the object, so if a parameter is not specified, the function can fall back to a predefined value.
- Clarity: When functions have multiple parameters, each with its meaning, it can be challenging to remember the order or purpose of each parameter. A parameter object provides a clear and structured way to pass and access parameters.
- Optional Parameters: It's easy to make parameters optional by not including them in the object if they are not needed for a particular call. This can simplify function calls.
- Reduced Function Signature Complexity: Using a single parameter object can help keep the function signature simple, especially when there are many parameters.
All of this with zero runtime overhead.
// Definition
fn example(p: ParamType) => ReturnType {
// Function body
// ...
return returnValue
}
// Call
example({
id: `1234`
value: `Value`
})
Pure functions cannot call other non-pure functions inside it.
// Declaration & Definition
fn example(p: T): ReturnType => {
let n = getSomethingPure()
// Do something with n
// Function body
return ReturnType
}
// Usage
example({})
Impure functions can call both pure & impure functions inside it, and are assumed to contain side effects.
fn example(p: T): ReturnType | Void -> {
let something = getSomethingPure()
processImpure(&something)
}
Generator functions are used to yield results generatively, whenever they are needed.
fn infinite(p: T): ReturnType /> {
let n = 0
loop {
yield n
n = F.increment(n)
}
}
log`$infinite().next()` // 0
log`$infinite().next()` // 1
Async functions return a promise to be delivered, when called, they are not blocking the further execution no matter how long the main thread has to await for the result to come.
// Declaration & Definition
fn example(p: T): ReturnType |> {
// Function body
}
// Usage
await example()
// Declaration & Definition
fn example(p: T): ReturnType ||> {
// Function body
}
// Usage
const promise = await example().next()
await promise.next()
await promise.pause()
await promise.cancel()
Anonymous lamda functions are short-lived functions that are bound to a callback of another function, and cannot be declared somewhere else.
They can be one of types:
- Syncronous
- Asynchronous
// Syncronous
object.map((p: T): ReturnType => {
// Function body
})
// Asynchronous
object.map((p: T): ReturnType |> {
// Function body
})
By default, every function receives one and only one object parameter and under the hood will expand this to individual arguments for the assembly interpretation where applicable.
But oftentimes there is need get more freedom in how a certain function is called.
// Declaration & Definition
@F::curry(3)
fn example(p1: T1, p2: T2, p3: T3) => {
// Implementation
}
// Usage
example(1, 2, 3)
example(1, 2)(3)
example(1)(2, 3)
example(1)(2)(3)
The fn
method is an alias for F
blueprint primitive that is used for all functions.
// Log an empty pure function
log`$F`
// Create and call a pure function, returns void result
log`$F()`
The F
constructor has many statically available functional methods that can be used natively in any form of loosely-coupled composability.
// Function declaration can be assigned a result of any functional operation.
fn getName = F::prop(`name`)
getName({
name: `Jon`
})
Then more complex functional composability can be used.
// Using piping (Top to botton)
fn getMangledUserId = F::pipe(
F::prop(`id`)
F::prefix(`ID_`)
)
// Or using composability (Bottom to top)
fn getMangledUserId = F::compose(
F::prefix(`ID_`)
F::prop(`id`)
)
// Usage
getMangledUserId({
id: `0000-0001`
})
// Using convergence
fn getMangledUserId = F::converge(F::concat)([
F::constant(`___`)
F::compose(
F::prefix(`ID_`)
F::prop(`id`)
)
])
// Usage
getMangledUserId({
id: `0000-0001`
})
Decorators are functional blueprints to vertically extend other building blocks in a non-destructive manner.
dc DecoratorName(&self) {
let new: T = `Something`
constructor(props: DecoratorNameProps) {
// Do something
}
}
@DecoratorName({
bColoredLog: true
})
fn someFunction() => {
// Implementation
}
@DecoratorName({
bColoredLog: true
})
bp BlueprintName(&self) {
// Implementation
}
bp BlueprintName(&self) {
field1: FieldType1
field2: FieldType2
// ...
constructor(p: BlueprintNameProps) {
// Constructor implementation
}
fn method1(p1: ParamType1) => ReturnType1 {
// Method implementation
}
fn method2(p2: ParamType2) => ReturnType2 {
// Method implementation
}
}
Methods are functions bound to a blueprint scope.
There can be two types of methods:
// Declaration & Definition
bp BlueprintName(&self) {
fn method(p1: ParamType1) => ReturnType { // 👈 This is a bound method as it's a direct child of the owning `bp`
// Method implementation
return ReturnType
}
}
// Usage
let b = BlueprintName()
b.method()
Static methods can be called directly from the blueprint avoiding the need to instantiate it, they cannot access the &self
reference inside them.
bp BlueprintName(&self) {
# @Static
fn method(p1: ParamType1) => ReturnType { // 👈 This is static method declared in the `Static` section `bp`
// Method implementation
return ReturnType
}
}
// Usage
BlueprintName::method() // 👈 The call to a static method is done via `::`
ℹ️ Accessing a static blueprint method encapsulated inside a namespace looks like this:
Namespace:Blueprint::staticMethod()
Access modifiers with zero runtime overhead, compile time memory alignment and performance optimizations.
By design there are 2 ways of looking at access modifiers.
All fields and methods are visible to the consumers by default
bp Parent(&self) {
fullName: String
bankCredentials: String
skeletsInTheCloset: String
}
Fields and methods are granularly grouped in a vertical manner
// Public
---
// Private
---
// Protected
If you only need Public
and Protected
// Public
---
---
// Protected
If you only need Protected
---
---
// Protected here, ignore the rest.
bp Parent(&self) {
fullName: String
---
bankCredentials: String
---
skeletsInTheCloset: String
}
In vex there are two ways of coupling blueprints.
- Strict via blueprint parent inheritance
- Loose via blueprint dependency composition
Dependency Inheritance is done by using the :
symbol between the Child
and the Parent(s)
ℹ️ When using inheritance, all the properties of the parents would be accessible or in other words, merged, in the
&self
reference.
// Single parent
bp Child(&self) : Parent {
fieldName: String
constructor(props: ChildProps) {
log`&self.bankAccount`
}
}
// Many parents
bp Child(&self) : [Father, Mother] {
name: String
constructor(props: ChildProps) {
log`&self.maleTraits`
log`&self.femaleTraits`
}
}
bp Child(&self) : [Parent1, Parent2, Parent3] {
name: String
constructor(props: ChildProps) {
log`&self.uniqueField1`
log`&self.uniqueField2`
log`&self.uniqueField3`
}
}
Dependency composition is done by using the ~
symbol between the Child
and the Sibling(s)
ℹ️ When connected to a blueprint, the sibling object will be injected into the
&self
reference and the name would be the camelCase version of the blueprint itself.
// Single sibling
bp Child(&self) ~ Sibling {
fieldName: String
constructor(props: ChildProps) {
log`&self.sibling.anotherField`
}
}
// Many siblings
bp Child(&self) ~ [Sibling1, Sibling2] {
fieldName: String
constructor(props: ChildProps) {
log`&self.sibling1.toy1`
log`&self.sibling2.toy2`
}
}
en Enumeration [
ONE
TWO
THREE
]
// Usage
Enumeration.ONE
Enumeration.TWO
tb Table [
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
]
Aliases are a way to create zero-overhead alternative way on accessing a certain function, blueprint or field.
// Single alias
fn example() => {
// Implementation
} as ex
// Many aliases
fn akwardlyLongFunctioName() => {
// Implementation
} as [alfnx, alfn, af]
// 👇 All of theese point to one memory address
akwardlyLongFunctioName()
alfnx()
alfn()
af()
// Pointing to an existing alias anywhere in the context of your app/lib
// src/a.x
fn example() => {
// Implementation
} as ex
// src/b.x
fn example() => {
// Implementation
} as ex
// 👆 Compilation error: The alias `ex` already exists in [...]
ℹ️ Blueprint aliases must start with a capital letter. Aliases are case-sensitive.
// Single alias
bp Example(&self) {
// Implementation
} as Ex
// Many aliases
bp Example(&self) {
// Implementation
} as [Exmp, Exm, Ex, EX] // 👈 Valid, as it's case sensitive
bp Example(&self) {
// Single alias
help: String = `Unique` as h
// Many aliases
group: String = `Unique` as [grp, gr, g]
}
// 👇 All the below are valid
Example().help
Example().h
Example().group
Example().grp
Example().gr
Example().g
import `@scope/name` as Alias
import { Something as Alias } from `@scope/name`
// Usage
Alias()