🏡 go home...
🗃️ go back...

magenta

thumbnail (magenta.png)
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

official website

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;

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...

🗃️ go back...

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]
// 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"