Oxide Programming Language

Overview

Oxide Programming Language

Interpreted C-like language with a Rust influenced syntax. Latest release

Example programs

/// recursive function calls to compute n-th
/// fibonacci sequence number

fn fib(n: int) -> int {
    if n <= 1 {
        return n;
    }

    return fib(n - 2) + fib(n - 1);
}

let nums = vec<int>[];
for let mut i = 0; i < 30; i += 1 {
    nums.push(fib(i));
}
/// sorting a vector using
/// insertion sort

fn insertion_sort(input: vec<int>) {
    for let mut i = 1; i < input.len(); i += 1 {
        let cur = input[i];
        let mut j = i - 1;
    
        while input[j] > cur {
            let temp = input[j + 1];
            input[j + 1] = input[j];
            input[j] = temp;
      
            if j == 0 {
              break;
            }
    
            j -= 1;
        }
    }
}

let input: vec<int> = vec[4, 13, 0, 3, -3, 4, 19, 1];

insertion_sort(input);

println(input); // [vec] [-3, 0, 1, 3, 4, 4, 13, 19]
/// structs

struct Circle {                 // struct declaration
    radius: float,
    center: Point,
    tangents: vec<Tangent>,     // inner vector of structs
}

impl Circle {                   // struct implementation
    fn calc_area() -> float {
        return PI * self.radius * self.radius;
    }
  
    fn add_tangent(t: Tangent) {      
        self.tangents.push(t);
    }
}

struct Point {
    x: int,
    y: int,
}

struct Tangent {
    p: Point,
}

const PI = 3.14159;

let circle = Circle {     // struct instantiation
    radius: 103.5,
    center: Point { x: 1, y: 5 } ,             // inner structs instantiation
    tangents: vec[                             // inner vector of structs instantiation
        Tangent { p: Point { x: 4, y: 3 } },
        Tangent { p: Point { x: 1, y: 0 } },
    ],
};

let area = circle.calc_area();  // 33653.4974775

circle.add_tangent(
    Tangent { p: Point { x: 11, y: 4 } }  // third tangent added
);
/// first-class functions

let make_adder = fn (x: num) -> func {
    return fn (y: num) -> num {
        return x + y;
    };
};

let add5 = make_adder(5);
let add7 = make_adder(7);

println(add5(2)); // 7
println(add7(2)); // 9
/// compute the greatest common divisor 
/// of two integers using Euclids algorithm

fn gcd(mut n: int, mut m: int) -> int {
    while m != 0 {
        if m < n {
            let t = m;
            m = n;
            n = t;
        }
        m = m % n;
    }

    return n;
}

gcd(15, 5); // 5

More examples

Usage

Interpret a file

oxide [script.ox]

REPL

There is also a simplistic REPL mode available.

oxide

To print current Oxide version

oxide version

Quick Overview

Variables and Type System

There are ten types embodied in the language: nil, num, int, float, bool, str, func, vec, any and user-defined types (via structs). See type system

Each variable has a type associated with it, either explicitly declared with the variable itself:

let x: int;

let mut y: str = "hello" + " world";

let double: func = fn (x: num) -> num {
    return x * 2;
};

let human: Person = Person { name: "Jane" };

let names: vec<str> = vec["Josh", "Carol", "Steven"];

or implicitly inferred by the interpreter the first time it is being assigned:

let x = true || false; // inferred as "bool";

let mut y;
y = vec<bool>[]; // inferred as "vec<bool>";

let dog;
dog = Dog { name: "Good Boy"}; // inferred as "Dog";

Mutable variables cannot be assigned to a value of another type, unless they are of type any:

let mut s: str = "string";
s = vec[]; //! type error

let mut a: any = Rectangle { height: 10, width: 10 };
a.height = "string"; //! type error
a = 45.34; // valid

Mutable Variables vs Immutable ones

There are two possible ways of declaring a variable: immutable and mutable. Immutable ones cannot be reassigned after having been assigned to a value.

let x: str = "string";

let y: num;
y = 100;

However, mutable ones behave like you would expect a variable to behave in most other languages:

let mut x: str = "hello";
x += " world";
x += "another string";

Shadowing

One important thing is that variables can be redeclared in other words, shadowed. Each variable declaration "shadows" the previous one and ignores its type and mutability. Consider:

let x: int = 100;
let x: str = "This was x value before: " + x;
let x: vec<any> = vec[];

Control Flow and Loops

If

if statement is pretty classic. It supports else if and else branches. Parentheses are not needed around conditions. Each branch must be enclosed in curly braces.

if x >= 100 {
    println("x is more than 100");
} else if x <= 100 && x > 0 {
    println("x is less than 100, but positive");
} else {
    println("x a non-positive number");
}

Match

match expression returns the first matching arm evaluated value. Unlike other control flow statements, match is an expression and therefore must be terminated with semicolon. It can be used in any place an expression is expected.

let direction = match get_direction() {
    "north" => 0,
    "east" => 90,
    "south" => 180,
    "west" => 270,
};

match true can be used to make more generalised comparisons.

let age = 40;

let description: str = match true {
    age > 19 => "adult",
    age >= 13 && x <= 19 => "teenager",
    age < 13 => "kid",
};

While

There are three loops in Oxide: while, loop and for. All loops support break and continue statements. Loop body must be enclosed in curly braces.

while statement is rather usual.

while x != 100 {
    x += 1;
}

Loop

loop looks like Rust's loop. Basically it is while true {} with some nice looking syntax.

loop {
    i -= 1;
    x[i] *= 2;
    if x.len() == 0 {
        break;
    }
}

For

for loop is a good old C-like for statement, which comprises three parts. You should be familiar with it.

for let mut i = 0; i < v.len(); i += 1 {
    println(v[i]);
}

// the first or the last parts can be omitted
let mut i = 0;

for ; i < v.len(); {
    println(v[i]);
    i += 0;
}

// or all three of them
// this is basically "while true" or "loop"
let mut i = 0;

for ;; {
    println(v[i]);
    i -= 1;

    if i < v.len() {
        break;
    }
}

Functions

Declared functions

Functions are declared with a fn keyword. Function signature must explicitly list all argument types as well as a return type. Functions that do not have a return value always return nil and the explicit return type can be omitted (same as declaring it with -> nil)

fn add(x: num, y: num) -> num {
    return x + y;
}

let sum = add(1, 100); // 101

fn clone(c: Circle) -> Circle {
    return Circle {
        radius: c.radius,
        center: c.center,
        tangents: vec<Tangent>[],
    };
}

let cloned = clone(circle);

// since this function returns nothing, the return type can be omitted
fn log(level: str, msg: str) {
    println("Level: " + level + ", message: " + msg);
}

Redeclaring a function will result in a runtime error.

Closures and Lambdas

Functions can also be assigned to variables of type func. As with other types, it can be inferred and therefore omitted when declaring a variable.

/// function returns closure
/// which captures the internal value i
/// each call to the closure increments the captured value
fn create_counter() -> func {
    let mut i = 0;

    return fn () {
        i += 1;
        println(i);
    };
}

let counter: func = create_counter();

counter(); // 1
counter(); // 2
counter(); // 3

Functions are first-class citizens of the language, they can be stored in a variable, passed or/and returned from another function.

fn vec_concat(prefix: str, suffix: str) -> str {
    return prefix + suffix;
}

fn str_transform(callable: func, a: str, b: str) -> any {
    return callable(s1, s2);
}

str_transform(str_concat, "hello", " world");

Defining a function argument as mut lets you mutate it in the function body. By default, it is immutable.

fn inc(mut x: int) -> int {
    x += 1;
    return x;
}

// same as with shadowing
fn inc(x: int) -> int {
    let mut x = x;
    x += 1;
    return x;
}

Immediately Invoked Function Expressions, short IIFE, are also supported for whatever reason.

(fn (names: vec<str>) {
    for let mut i = 0; i <= names.len(); i += 1 {
        println(names[i]);
    }
})(vec["Rob", "Sansa", "Arya", "Jon"]);

Structs

Structs represent the user-defined types. Struct declaration starts with struct keyword. All struct properties are mutable and public by default.

struct Person {
    name: str,          // property of type str
    country: Country,   // property of type struct Country
    alive: bool,
    pets: vec<Animal>,  // property of type vector of structs Animal
}

struct Animal {
    kind: str,
    alive: bool,
}

struct Country {
    name: str,
}

Impl blocks

Struct implementation starts with impl keyword. While struct declaration defines its properties, struct implementation defines its methods. self keyword can be used inside methods and points to the current struct instance.

impl Person {
    fn change_name(new_name: str) {
        self.name = new_name;
    }
  
    fn clone() -> Person {
        return Person {
            name: self.name,
            country: self.country,
            alive: self.alive,
            pets: self.pets,
        };
    }
}

You instantiate a struct creating it with curly braces and initializing all its properties Animal { prop: value[, prop: value ...] }.

let cat = Animal {
    kind: "cat",
    alive: true
};

let john: Person = Person {
    name: "John",
    alive: true,
    pets: vec[cat],                    // via variable
    country: Country { name: "UK" }    // via inlined struct instantiation
};

Dot syntax is used to access structs fields and call its methods.

// set new value
john.country = Country { name: "USA" };
john.pets.push( Animal {
    kind: "dog",
    alive: true
});

// get value
println(john.pet[0].kind); // cat

// call method
let cloned = john.clone();
cloned.change_name("Jonathan");

Immutable variable will still let you change the struct's fields, but it will prevent you from overwriting the variable itself. Similar to Javascript const that holds an object.

john.name = "Steven";    // valid, John is not a John anymore
john.pet.kind = "dog"    // also valid, john's pet is changed
john = Person { .. };    // invalid, "john" cannot point to another struct

Structs are always passed by reference, consider:

fn kill(person: Person) {
    person.alive = false;     // oh, john is dead
    person.pet.alive = false; // as well as its pet. RIP
}

kill(john);

Vectors

Vectors, values of type vec<type>, represent arrays of values and can be created using vec<type>[] syntax, where type is any Oxide type.

Vectors support following methods:

  • vec.push(val: any) push value to the end of the vector

  • vec.pop() -> any remove value from the end of the vector and return it

  • vec.len() -> int get vectors length

let planets = vec<str>["Mercury", "Venus", "Earth", "Mars"];

let mars = planets[3];

planents.push("Jupiter");     // "Jupiter" is now the last value in a vector

let jupiter = planets.pop(); // "Jupiter" is no longer in a vector.

planets[2] = "Uranus";       // "Earth" is gone. "Uranus" is on its place now

planets.len();               // 3

typeof(planets);             // vec<str>

When type is omitted it is inferred as any on type declaration, but on vector instantiation it is inferred as proper type when possible. Consider:

let v: vec = vec[];                     // typeof(v) = vec<any>
let v = vec[];                          // typeof(v) = vec<any>

let v: vec<int> = vec[1, 2, 3];         // typeof(v) = vec<int>
let v = vec<int>[1, 2, 3];              // typeof(v) = vec<int>
let v = vec[1, 2, 3];                   // typeof(v) = vec<int>, because all the initial values are of type "int"

let v = vec<bool>[true];                // typeof(v) = vec<bool>

let v: vec<Dog> = vec[                  // typeof(v) = vec<Dog>, type declaration can actually be omitted
    Dog { name: "dog1" },
    Dog { name: "dog2" },
];

let v = vec[                            // typeof(v) = vec<vec<Point>,
    vec[                                // inferred by the initial values, despite the type being omitted
        Point { x: 1, y: 1 }, 
        Point { x: 0, y: 3 } 
    ],
    vec[ 
        Point { x: 5, y: 2 }, 
        Point { x: 3, y: 4 } 
    ],
];


let matrix = vec[                       // typeof(v) = vec<vec<int>>
    vec[1, 2, 3, 4, 5],
    vec[3, 4, 5, 6, 7],
    vec[3, 4, 5, 6, 7],
];

let things = vec[nil, false, Dog {}];   // typeof(v) = vec<any>

Like structs vectors are passed by reference. Consider this example of an in place sorting algorithm, selection sort, that accepts a vector and sorts it in place, without allocating memory for a new one.

Selection sort
fn selection_sort(input: vec<int>) {
    if input.len() == 0 {
        return;
    }

    let mut min: int;
    for let mut i = 0; i < input.len() - 1; i += 1 {
        min = i;

        for let mut j = i; j < input.len(); j += 1 {
            if input[j] < input[min] {
                min = j;
            }
        }

        if min != i {
            let temp = input[i];
            input[i] = input[min];
            input[min] = temp;
        }
    }
}

Vectors can hold any values and be used inside structs.

let animals: vec = vec[
    Elephant { name: "Samuel" },
    Zebra { name: "Robert" },
    WolfPack { 
        wolves: vec<Wolf>[
            Wolf { name: "Joe" },
            Wolf { name: "Mag" },
        ]
    }
]

let mag = animals[2].wolves[1]; // Mag

Trying to read from a non-existent vector index will result in a uninit value returned. Trying to write to it will result in an error.

Constants

Constants unlike variables need not be declared with a type since it can always be inferred. Constants also cannot be redeclared, and it will result in a runtime error. Constants must hold only a scalar value: str, int, float, bool.

const MESSAGE = "hello world";

const PI = 3.14159;

const E = 2.71828;

Operators

Unary

  • ! negates boolean value
  • - negates number

Binary

  • &&, || logic, operate on bool values
  • <, >, <=, >=, comparison, operate on int, float values
  • ==, != equality, operate on values of the same type
  • -, /, +, *, % math operations on numbers
  • + string concatenation, also casts any other value in the same expression to str
  • =, +=, -=, /=, %=, *= various corresponding assignment operators

Comments

Classic comments that exist in most other languages.

// inline comments

/*
    multiline comment
 */

let x = 100; /* inlined multiline comment */ let y = x;

Standard library

A small set of built-in functionality is available anywhere in the code.

  • print(msg: str) prints msg to the standard output stream (stdout).

  • println(msg: str) same as print, but inserts a newline at the end of the string.

  • eprint(err: str) prints err to the standard error (stderr).

  • eprintln(err: str) you got the idea.

  • timestamp() -> int returns current Unix Epoch timestamp in seconds

  • read_line() -> str reads user input from standard input (stdin) and returns it as a str

  • file_write(file: str, content: str) -> str write content to a file, creating it first, should it not exist

  • typeof(val: any) -> str returns type of given value or variable

Comments
  • Analyser

    Analyser

    Proof of concept of #8

    Tasks:

    • [x] TypeCheck Functions
    • [x] Mutability of Variables
    • [x] Undefined Variable Access
    • [x] Access in Scope
    • [x] Property Access
    • [x] Structs (Declaration and Calls)
    • [ ] Impl block
    • [ ] Integrate with stdlib
    • [ ] Report all errors
    • [ ] Improve TypeError Messages
    • [ ] Add lint option (using --lint)
    • [ ] Enums
    • [ ] Produce typed AST

    @tuqqu, I will add more items to the list as I work on it. It would be great to hear any suggestions that you have.

    opened by 54k1 8
  • Introduce an Analyser phase

    Introduce an Analyser phase

    Taking advantage of the strongly typed nature of oxide, I propose the following:

    1. Type checking phase: Invalid operations on expressions such as -"asdf", !12 are being checked at runtime, when they can be checked statically because we already know the types and their operations. This would involve writing a static type checker. I suspect, however, that this has an issue due to the any type. The current behavior, lets a variable with the type any to take on values of other types without an explicit cast. Due to this, it would be hard to reason about operations on such a variable. Therefore, we can't really get away with runtime checks due to any, unless of course, we change the way it behaves.

    I would prefer an explicit cast when applying operators to variables of the type any. For instance,

    let x: any;
    x = 12;
    let y = (x as int) + 12;
    

    We can introduce a lot more checks before the interpreter is run, leading to reporting more errors before the interpreter is run (at runtime), and also better performance(by getting away with unnecessary checks):

    • Accessing invalid properties of a struct instance.
    struct Person {
        pub name: String
    }
    let p: Person = Person::new();
    println(p.age);
    

    If p were of the type any, it would be very difficult and probably inefficient(in the worst case) to reason about the invalid property access.

    • Same as the above with enum values.
    • Matching argument types of a function call to its declaration (signature).
    1. Apart from type checking, we can perform some basic analyses for things such as:
    • Arity match check for function calls.
    • Prevent re-declarations of struct/enums.
    fn f() {
        fn g() {}
        enum E {}
    }
    f();
    f();
    

    In this case, any declaration inside a block is re-declared each time the block is run. The analyzer can possibly prune such declarations and hoist it at the top level.

    @tuqqu please let me know what you think about these. Also, please feel free to correct me wherever necessary. Is this in line with the vision you have for oxide?

    opened by 54k1 4
  • Introduce operator enum in Parser

    Introduce operator enum in Parser

    Currently, for binary and unary expressions, the operator is being set as a Token. However, this can be simplified by introducing new enum types: BinaryOperator, UnaryOperator, AssignmentOperator, SetIndexOperator.

    Example:

    enum BinaryOperator {
        Add, Sub, Div, Mul, Mod, Equality, // ... etc
    }
    enum UnaryOperator {
        Add, Sub
    }
    
    // Binary Expression would be
    struct Binary {
        pub left: Box<Expr>,
        pub right Box<Expr>,
        pub operator: BinaryOperator
    }
    // And similarly Unary Expression, and so on..
    
    1. These would be less bloated than having a token in each expression.
    2. Less Error prone since you get compile time check when setting the operator in the expressions. For instance, you can't set the BinaryOperator to some arbitrary token. I'm sure the appropriate checks are being made, however, this is cleaner IMO.

    What do you think @tuqqu?

    opened by 54k1 3
  • Use rustc's lexer

    Use rustc's lexer

    It is independent of the rest of rustc, used by rust-analyzer and available as rustc-ap-rustc_lexer on crates.io. Using rustc's lexer may save a bit on code maintenance and will make oxide's syntax a bit closer to rust's.

    opened by bjorn3 2
  • Declaring structs/enums in function scope

    Declaring structs/enums in function scope

    I tried to perform the following:

    fn f () {
        enum E {}
    }
    
    enum E {}
    

    And, I got the following error message: Error E: Name "E" is already in use at [5:5].

    Should this be the behavior? struct/enum declarations in a scope must become inaccessible when out of scope. (In this case, they are inaccessible, however, it won't let us declare another enum/struct with the same name) The check for previously declared enum/struct is being done inParser::check_if_declared. We should probably move it to the interpreter where the name resolution by scope is handled.

    @tuqqu what do you think about this? What should be the behavior?

    opened by 54k1 2
  • Usage of `Self` in impl blocks

    Usage of `Self` in impl blocks

    I tried to use Self in a method in the following manner and could not get it to work

    struct Circle {
        radius: float
    }
    
    impl Circle {
        fn larger_circle() -> Self {
            return Self {
                radius: self.radius()+10
            }
        }
    }
    

    I think there are two issues here.

    1. Self cannot be used as return type.
    2. Self cannot be used in call expressions.

    Solutions:

    1. Add a new variable to parser to keep track of the struct which the current impl block points to. We could call it current_struct_decl maybe, and it can be an optional. It must be set whenever we come across an impl block, and reset once out of the impl block.
    struct Circle {...}
    impl Circle {
    // set current_struct_decl to Circle
    }
    // set current_struct_decl to None
    
    1. Update consume_type to support Self. We need to add a check for Self and return the appropriate struct_decl in case we are in an impl block. If we are not in an impl block, then Self does not make sense, and we need to raise an error.
    2. In call_expr , currently, only the lexeme is being checked if it is present in the structs. impl has the lexeme "impl", and therefore is not matched in the structs array. A match for Self also must be checked in this case.

    Please let me know if I'm missing anything in these issues and their proposed solutions. I can take this issue up once you approve. Very cool project btw! :)

    opened by 54k1 2
  • Add Unary Operator

    Add Unary Operator

    • Introduce enum UnaryOperator in parser/mod.rs, and update the existing UnaryExpression structure to use UnaryOperator.
    • Update the implementation in interpreter(eval_unary_expr) to use the new UnaryOperator.
    • Update the parser (unary_expr) to accomodate unary addition operation (Ex: +1).
    opened by 54k1 0
Releases(v0.7)
  • v0.7(May 11, 2021)

  • v0.6(May 1, 2021)

    • Self in call expressions
    • static methods
    • enums with impl blocks
    • fn(T) -> T type
    • traits for structs
    • bitwise operations
    • type casting with as
    • any type requires explicit casting
    • for ... in loops
    • entry point main() function
    • dbg added to stdlib
    • bug fixes & improvements
    Source code(tar.gz)
    Source code(zip)
  • v0.5(Apr 5, 2021)

Owner
Arthur Kurbidaev
Arthur Kurbidaev
A computer programming language interpreter written in Rust

Ella lang Welcome to Ella lang! Ella lang is a computer programming language implemented in Rust.

Luke Chu 64 May 27, 2022
The hash programming language compiler

The Hash Programming language Run Using the command cargo run hash. This will compile, build and run the program in the current terminal/shell. Submit

Hash 13 Nov 3, 2022
🍖 ham, general purpose programming language

?? ham, a programming language made in rust status: alpha Goals Speed Security Comfort Example fn calc(value){ if value == 5 { return 0

Marc Espín 19 Nov 10, 2022
A small programming language created in an hour

Building a programming language in an hour This is the project I made while doing the Building a programming language in an hour video. You can run it

JT 40 Nov 24, 2022
The Loop programming language

Loop Language Documentation | Website A dynamic type-safe general purpose programming language Note: currently Loop is being re-written into Rust. Mea

LoopLanguage 20 Oct 21, 2022
Stackbased programming language

Rack is a stackbased programming language inspired by Forth, every operation push or pop on the stack. Because the language is stackbased and for a ve

Xavier Hamel 1 Oct 28, 2021
REPL for the Rust programming language

Rusti A REPL for the Rust programming language. The rusti project is deprecated. It is not recommended for regular use. Dependencies On Unix systems,

Murarth 1.3k Dec 20, 2022
sublingual: toy versions of existing programming languages

sublingual: toy versions of existing programming languages This is a collection of toy languages created by taking much "larger" languages (e.g. Rust)

Eduard-Mihai Burtescu 20 Dec 28, 2022
A rusty dynamically typed scripting language

dyon A rusty dynamically typed scripting language Tutorial Dyon-Interactive Dyon Snippets /r/dyon Dyon script files end with .dyon. To run Dyon script

PistonDevelopers 1.5k Dec 27, 2022
A static, type inferred and embeddable language written in Rust.

gluon Gluon is a small, statically-typed, functional programming language designed for application embedding. Features Statically-typed - Static typin

null 2.7k Dec 29, 2022
Lisp dialect scripting and extension language for Rust programs

Ketos Ketos is a Lisp dialect functional programming language. The primary goal of Ketos is to serve as a scripting and extension language for program

Murarth 721 Dec 12, 2022
Source code for the Mun language and runtime.

Mun Mun is a programming language empowering creation through iteration. Features Ahead of time compilation - Mun is compiled ahead of time (AOT), as

The Mun Programming Language 1.5k Jan 9, 2023
Rhai - An embedded scripting language for Rust.

Rhai - Embedded Scripting for Rust Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripti

Rhai - Embedded scripting language and engine for Rust 2.4k Dec 29, 2022
Interpreted language developed in Rust

Xelis VM Xelis is an interpreted language developed in Rust. It supports constants, functions, while/for loops, arrays and structures. The syntax is s

null 8 Jun 21, 2022
Interactive interpreter for a statement-based proof-of-concept language.

nhotyp-lang Nhotyp is a conceptual language designed for ease of implementation during my tutoring in an introductive algorithmic course at Harbin Ins

Geoffrey Tang 5 Jun 26, 2022
Scripting language focused on processing tabular data.

ogma Welcome to the ogma project! ogma is a scripting language focused on ergonomically and efficiently processing tabular data, with batteries includ

kdr-aus 146 Dec 26, 2022
Sodium Oxide: Fast cryptographic library for Rust (bindings to libsodium)

sodiumoxide |Crate|Documentation|Gitter| |:---:|:-----------:|:--------:|:-----:|:------:|:----:| |||| NaCl (pronounced "salt") is a new easy-to-use h

sodiumoxide 642 Dec 17, 2022
Magnesium-Oxide (MGO) - a secure file uploader with support for ShareX.

A blazingly fast, ShareX uploader coded in Rust (using actix web) which utilizes AES-256-GCM-SIV to securely store uploaded content.

Magnesium 26 Nov 25, 2022
OXiDE - A PoC packer written in Rust

OXiDE is a PoC Rust packer. It doesn't do much other than compress the target binary, but if you read the code, you'll find that extending it to do more (e.g., obfuscation, anti-reversing) is very possible!

frank2 44 Nov 30, 2022
Magnesium-Oxide (MGO) a secure file uploader with support for ShareX.

A blazingly fast, ShareX uploader coded in Rust (using actix web) which utilizes AES-256-GCM-SIV to securely store uploaded content.

Magnesium 26 Nov 25, 2022