magenta
created 2025/05/05 category software views 159
magenta is a mid-level type-safe language. it's immutable where convenient, does not have null, and aims to keep code as short as possible whilst maintaining not being absolutely ridiculous. the syntax itself borrows elements from the bourne shell, f#, typescript, and go
draft spec
this is the latest draft the magenta lanugage specification. i designed it when i was 15 years old
namespacing
at the top of each magenta source file, magenta expects a namespace. at compile time the working directory will be scanned for magenta files and the one in the namespace `main` will be chosen as the entry point. there may be multiple files belonging toX a certain namespace, but only one file may be in the `main` namespace!
namespace main
then after the `namespace` decleration, you may include libraries
// include libraries and namespaces include tty include err include math include loop include arrays include myUtility // eg. some other namespace in the project folder // can be written in one neat line include tty err math loop arrays myUtility
you may alias an included namespace, but then you cannot include any other namespaces in that line
include myUtility.errors as myErrors // these are equivalent myUtility.errors.GayError myErrors.GayError
it can also be useful when writing a library
namespace myLib include myLib.errors as errors
types
we have all the classics: `bool`, `string`, (`u`)`int`(`8`|`16`|`32`|`64`), `float`, and `double`
`int` and `uint` imply `int16` and `uint16` by default
you can also use `byte` and `sbyte` for `uint8` and `int8` respectively
to represent an array, simply append a couple of square brackets to the end of an existing type (ie. `type[]`)
you can create multi-dimensional arrays by adding multiple couples of brackets, ie. `type[][]` is 2d, `type[][][]` is 3d, etc.
magenta does not have null (explained in the *default values* section) but does have a `void` type
`void` can be used as a return type of a function, but it cannot be used as a type for a variable/field
functions + function types
there is an `func` type to represent functions. by default it represents a function that returns `void` and takes no args. you can specify a return type by appending `<type>`, specify a list of argument types by appending `[type, type, ...]`, and specify a `throws` with `!error`. for example;
int divide(int x, int y) throws MathError {
y == 0 && throw "divide by zero"
return x / y
}
typeof(divide)
→ func[int, int]!MathError
more information about errors in the `error handling` section
bools
the value of a `bool` can only be `true` or `false`
the bool has magenta's only unary operator, the `!` (not)
!false or false! → true !true or true! → false let myBool bool; !myBool or myBool! → true !isArray([]) or isArray([])! → false
there is no "internal truthiness" that may be revealed with `!`. it is only allowed next to booleans, and will error if placed next to any other type
variables
variables can be defined by using a var statement in the format `var variableName T` (eg. `var name string`)
including the type in the statement is optional if you assign to the variable right away, as the type can be inferred
a constant can be defined by using a let statement in the format `let constantName T` (eg. `let age byte`), once a value is assigned to a constant it cannot be mutated
if you do not assign to the constant right away, it will assume its type's default value, and will let you set it later
assignment
to assign to a variable, constant, or field you can use the arrow syntax in the format `value -> variable` (eg. `"hunter" -> var name` or `"joe" -> instaceOfAPersonStruct.firstName`)
as magenta always executes left to right, you may not use `<-` to make "reverse assignments"
piping
the pipe character `|` pipes the expression on the left to the one on the right, ie. `from | to`, usually `to` is a `func` but other types allow piping into them as well
32 + 32 | math.sqrt → 8 32 + 32 | math.sqrt | math.itoa → "8"
you can only pipe one argument at once, if you need to pipe to a function that takes multiple required arguments, you can use a dollar sign as one of the arguments to represent the piped value
4 | math.pow()
default values
magenta lacks the concept of any `nil` or `undefined` values. instead, when a variable or field is defined without an explicit value, it takes on its "default value". a few examples;
- `var x string`: `x` has the value of empty string (`""`)
- `var y int`: `y` has the value of zero (`0`)
- `var z Guy`: a struct is initialized into `z` with default values for all of its members (`new Guy()`). **this will error if the struct has any `required` fields**
conditionals
magenta has the following conditionals
any == any int >= int any != any int <= int int > int bool && bool int < int bool || bool
as logical expressions are evaluated left to right, they are tested for possible "short-circuit" evaluation
`a() && b()` would not call `b` if `a` returned false
`a() || b()` would not call `b` if `a` returned true
magenta has if statements and ternaries that work just like how they would in any other c-style language
arithmetic
magenta has the following math operators, which can only be used for numerical values of the *same type*
when dividing integers, anything after the decimal point will be cut off
x + y x * y x - y x ^ y // power (xʸ) x / y x % y // remainder (of x⁄y)
there is a shorthand for assignment with a single operation, `y $ x -> y`, with `x hB> y` where `=> / 🏡 go home...
is an operator above, eg. `x ^-> y` == `y ^ x -> y`
arithmetic in magenta is done left to right, there is no operator precedence
// bimdas 100 + 30 * 3 → 133 100 + (30 * 3) → 133 (100 + 30) * 3 → 390 // magenta 100 + 30 * 3 → 390 100 + (30 * 3) → 133 (100 + 30) * 3 → 390
bimdas order - ( ), ^, %, *, /, +, -
in magenta, brackets do not get calculated first
if statements
magenta has your regular if/else statements
if condition {
} else condition {
} else {
}
thanks to short circuit evaluation, you can utilize `&&` and `||` as shorthand for `if condition` and `if !condition` respectively
switch statements
then we have switch statements. switch statements comprimise a set of cases that follow a function call, a case has the syntax
start condition separator [end]
`start` is either `|>` for a single line case, or `}>` to end a multi line case
`condition` is an expression that has the output of the function piped into it
`separator` is either `:` to start a single line case, or `{` to start a a multi line case
you may `end` the final case with a single `}` if it is a multi line case
for example;
getInput("password")
|> "good" {
login()
return 0
}> "short" : tty.writeln("too short")
|> "long" : tty.writeln("too long")
|> else {
tty.writeln("password is otherwise invalid")
// this last case could also be written as:
// |> else : tty.writeln("...")
}
loops
magenta two kinds of loops, the `for` and the `do` loop. the `for` loop behaves as a standard c-style `for` loop, but also doubles as a traditional `while` loop if you exclude the brackets
for (0 -> var i; i < 5; 1 +-> i) {
tty.writeln(i | math.itoa)
} → 0, 1, 2, 3, 4
0 -> var x
for x < 5 {
tty.writeln(1 +-> i | math.itoa)
} → 1, 2, 3, 4, 5
the `do` loop acts as a traditional c-style `do while` loop, executing the contents of the block at least once, but the statement stays in the same position
0 -> var x // x is a global variable
void loop() {
do x < 5 {
tty.writeln(1 +-> i | math.itoa)
}
}
loop()
→ 1, 2, 3, 4, 5
loop()
→ 6 // if a for loop had been used, there would have been no output this second time
enums
in magenta, an enum is an array of unique strings that when used as a type, restricts what contents a string might have, like in typescript
the format for an enum consists of an opening definition eg. `enum MyEnum {`, a set of strings eg. `"yes" "maybe" "possibly" "no"`, and a closing bracket followed by the index of the default value (not required, default is `0`)
eg. now you can type a function as returning `MyEnum`, which you can expect to return one of those strings
enum MyEnum {
"yes" "maybe" "possibly" "no" }
var x MyEnum
→ "yes" // x is initialized, by default using the first enum value
"maybe" -> x
// (value of x is updated)
"definitely" -> x
→ compiler error: "definitely" is not a value of MyEnum
error handling
any enum can be used as an error type, to be able to return an error, add a `throws` keyword onto your function definition followed by an enum, then you can now `throw` a string from that enum, for example;
enum NavigationError {"ok" "notExist" "toFile"}
string cd(path string) throws NavigationError {
stat(path) -> var pathStat
|> "notExist" : throw "notExist"
|> "file" : throw "toFile"
... // return the new full current path
}
to handle thrown errors, you can use a catch statement, which is identical to a switch statement, except using `!>` instead of `|>`. to use a switch statement after a catch statement, you can denote that you have stopped catching errors with `!> default |>`
cd("thisPathDoesNotExist")
!> "notExist" : tty.writeln("invalid path")return
!> "toFile" {
getFileParentFolder()
!> ... : ... // conditionals and/or further errors possible in blocks
return
}> default |> // it is cleaner to put the |> here
... : ...
|> ... { ...
}> ... { ...
}
if you just want something to catch errors but no error in particular, you can use the single line catch `!!`. a break/return statement is required at the end of the handling expression(s), as well as a semicolon
math.sqrt(-1) !! tty.println; -> var squareroot → prints "sqrt of negative number" → squareroot is not assigned to
structs
structs are defined in struct blocks. to make one first you open it with `struct StructName {`, then you can enter in a comma deliminated list of your struct's fields, and close it with `}`
a field has the following syntax (square brackets implying optional)
FieldName [const] [required] T [= defaultValue]
- `T` is optional when there is a default value
- if `const` is present, that field's value can only be set at initialization time
- if `required` is present, that field's value must be set at initialization time
- you can use either new lines or commas to deliminate fields
// example of a struct definition
struct Guy {
name = "guy", age int
gay const = true
awesome required const,
}
to initialize a struct, call it as a function with the new keyword, and assign it to a variable, ie. `new Struct(field0 value0, field1 value1, etc...) -> var myStruct`
to define a method for a struct, simply define a function as a field of a struct, ie. `T Struct.myMethod() { ... }`
you can define a constructor by writing a function with the return type and name of the corrosponding struct, ie. `Guy Guy() { ... }`
memory management
magenta is immutable where convenient, and has no references. there is no manual memory management, and it implements escape analysis as its sole garbage collection mechanism
// initializes memory for an unitialized struct
var globalHomer Guy
string getName() {
// initializes a new Guy with name "lisa" into new variable 'guy', assigning new memory
new Guy(name "lisa") -> var guy
// creates a copy of guy into a new variable 'otherGuy', assigning new memory
guy -> var otherGuy
"homer" -> guy.name // updates the name of the copy
// copies 'guy' over 'globalHomer', changing memory
guy -> globalHomer
"asdf" -> guy.name // update the name of the copy again
// 'otherGuy' does not reflect name changes of 'guy'
return otherGuy.name // "lisa"
// upon end of the function, all allocated memory is freed (in this case 'guy' and 'otherGuy')
}
// globalHomer has its default values
globalHomer.name | tty.println
// getName() is ran, returning the name of 'otherGuy'
// it also assigns 'guy' to 'globalHomer'
getName() | tty.println // "lisa"
// only the first update to 'guy' is reflected in 'globalHomer'
globalHomer.name | tty.println // "homer"