spec

The SafeScript Lang Specification

View on GitHub

The SafeScript Language Specification v1

This is the first version of the SafeScript language specification.

Creation Date: February 2, 2024 (INDEV)

Table of Contents

Introduction

SafeScript is a morphic language (yes, I did make that word up). That means that it can be both compiled and interpreted (as well as embedded). The language is designed to be safe, simple and fast. It is a statically typed language with an algebraic data type system (similar to Haskell). It is designed to be a scripting language, that is easily embeddable in other applications.

The language is designed to be simple and easy to learn, as it is based off of TypeScript.

The language is designed to be safe, thus it has no nulls, no undefineds, no exceptions and no runtime crashes (assuming no use of unwraps).

Safety

SafeScript is designed to be safe, thus it has no nulls, no undefineds, no exceptions and no runtime crashes (assuming no use of unwraps). This is achieved through the use of the Option and Result types. The use of these types is encouraged, as it makes the code safer and easier to reason about.

No Nulls / No Null Pointers / No Undefineds

Due to the fact that nulls are the source of many bugs, SafeScript does not have nulls, undefineds or null pointers. Instead, it uses the Option type to represent a value that may or may not be present.

Example:

let x: Option<num> = Some(5);
let y: Option<num> = None;

No Exceptions

SafeScript does not have exceptions, but that does not mean that it does not have error handling. Many other languages, have syntax for throwing and catching exceptions. try, catch, throw, raise, except, finally, etc. SafeScript does not have any of that. Instead, SafeScript uses the Result type to represent a value that may or may not be an error. Similar to rust, the Result type has two variants: Ok and Err.

Example:

let x: Result<num, str> = Ok(5);
let y: Result<num, str> = Err("An error occurred");

Unwraping

The use of the unwrap method is discouraged in production code, or performance critical code, as it can lead to crashes, and can be slow. However, it is extremely useful for prototyping, testing and debugging. As it can be used to quickly figure out what is wrong with the code and where.

Essentially, unwrap is the only way to cause a runtime crash in SafeScript. Thus, if you do not use unwrap, you will not have any runtime crashes. Obviously, that is not always easy, but it is possible.

Syntax

Primitives

SafeScript does not have many primitives, as it is designed to be simple. The primitives that it does have are: bool, num, str, char, and void. They are all denoted in lowercase for simplicity.

bool

The bool type is a primitive type that represents a boolean value. It can be either true or false.

Methods:

Trait Implementations:

Implementation Notes:

num

The num type is a primitive type that represents a number. It can be an integer or a floating point number, with arbitrary precision. (I will be using Malachite)

str

The str type is a primitive type that represents a sequence of characters. It is a UTF-8 encoded string. (Although the implementation can vary, it should NOT be a null-terminated string, for safety reasons.)

char

The char type is a primitive type that represents a single character. It is a Unicode scalar value.

void

The void type is a primitive type that represents the absence of a value. It is similar to the void type in TypeScript. Or the unit type in Rust.

Core

The standard library of SafeScript is called core. It contains the most basic types and functions. By default, the core module is imported into every SafeScript file, and can be accessed using the core namespace.

Example:

let x: core.Option<num> = core.Option.Some(5);

However, the most commonly used types and functions are re-exported in the core.prelude module, and can be accessed without the core namespace.

Example:

let a1: Option<num> = Some(5);
let a2: Option<num> = None;
let b1: Result<num, str> = Ok(5);
let b2: Result<num, str> = Err("An error occurred");
let c: str = "Hello, World!";
let d: char = 'a';
let e: bool = true;
let f: void;

core.prelude

The core.prelude module contains the most commonly used types and functions. It is re-exported in every SafeScript file, and can be accessed without the core namespace. It contains:

core.eh

The core.eh contains the error handling types and functions. Such as Result and Option. As well as the Error type.

core.eh.Result

The Result type is a type that represents a value that may or may not be an error. This is used all over the place in SafeScript. Especially in IO operations. This is one of the ways that SafeScript can guarantee safety.

Signature:

enum Result<O, E> {
    Ok(O),
    Err(E),
}

Generics:

Variants:

core.eh.Result.Ok

Represents a successful value.

core.eh.Result.Err

Represents an error value.

Methods:

core.eh.Option

The Option type is a type that represents a value that may or may not be present. It is supposed to be used in place of nulls, undefineds and null pointers. This is one of the ways that SafeScript can guarantee safety.

Signature:

enum Option<T> {
    Some(T),
    None,
}

Generics:

Methods:

core.eh.Error

The Error is a trait that all error types should implement. Implementing this trait allows for better error handling and interoperability. And allows for the use of short-circuiting early returns.

Example:

function read_file(path: str) -> Result<str, FileError> {
    // Code here...
}

function main() -> Result<(), dyn Error> {
    let contents = read_file("file.txt")?;
    // With the Error trait, we can use the ? operator to short-circuit early returns.
    // Otherwise, we would have to do this:
    // let contents = match read_file("file.txt") {
    //     Ok(contents) => contents,
    //     Err(err) => return Err(err),
    // };
}

Signature:

trait Error: fmt.Debug + fmt.Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

Methods:

Trait Implementation Requirements:

core.eh.panic

The panic function is a function that is used to cause a runtime crash. You should NEVER use this function. For exception-like error handling, use the Result type.

Signature:

fn panic(msg: str) -> ! { }

Parameters:

core.mem

The core.mem module contains functions for finer control over memory (while still being safe).

It contains things like Vec

core.mem.Error

Signature:

enum Error {
    OutOfMemory,
    InvalidPointer,
    InvalidSize,
    InvalidAlignment,
    BitConversionError,
}

Variants:

core.mem.Error.OutOfMemory

Indicates that the system is out of memory.

core.mem.Error.InvalidPointer

Indicates that the pointer is invalid.

core.mem.Error.InvalidSize

Indicates that the size is invalid.

core.mem.Error.InvalidAlignment

Indicates that the alignment is invalid.

core.mem.Error.BitConversionError

Indicates that there was an error converting bits.

core.mem.Result

A type alias for core::Result<T, Error>. Where T is the type of the value. And Error is core.mem.Error.

Signature:

type Result<T> = core.Result<T, Error>;

Generics:

core.mem.Vec