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 - Introduction to the SafeScript language.
- Safety - The safety features of SafeScript.
- No Nulls / No Null Pointers / No Undefineds - Absence of nulls, null pointers and undefineds.
- No Exceptions - How SafeScript does not have exceptions.
- Unwraping - The use of
unwrapfor bothOptionandResult.
- Syntax - The syntax of SafeScript.
- Primitives - The primitive types of SafeScript.
- Core - The standard library of SafeScript, also known as
core.core.prelude- The re-exported types and functions.
- Implementation Notes - Notes on the implementation of SafeScript.
- FAQ - Frequently asked questions about SafeScript.
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:
thenSome<T>(self, value: T) -> Option<T>- If the boolean istrue, returnsSome(value), otherwise returnsNone.then<T>(self, f: fn() -> T) -> Option<T>- If the boolean istrue, returnsSomecontaining the result of invokingf, otherwise returnsNone.
Trait Implementations:
core.fmt.Debug- Formats the boolean as"true"or"false".core.fmt.Display- Formats the boolean as"true"or"false".core.ops.Not- Returnstrueif the boolean isfalse, andfalseif the boolean istrue.core.ops.BitAnd<Ouput = bool>- Returnstrueif both booleans aretrue, andfalseotherwise.core.ops.BitAndAssign- Assignsselftoself.bitAnd(rhs).core.ops.BitOr<Ouput = bool>- Returnstrueif either of the booleans aretrue, andfalseotherwise.core.ops.BitOrAssign- Assignsselftoself.bitOr(rhs).core.ops.BitXor<Ouput = bool>- Returnstrueif the booleans are different, andfalseif they are the same.core.ops.BitXorAssign- Assignsselftoself.bitXor(rhs).
Implementation Notes:
- The
booltype is to be implemented as a single byte, with the value0representingfalse, and the value1representingtrue. This is to ensure that thebooltype is as efficient as possible. - Implementation is to have no look-up table attached to it.
That means that
thenSomeandthenare to be implemented as inline functions.
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:
O- The type of the Ok value.E- The type of the Err value.
Variants:
core.eh.Result.Ok
Represents a successful value.
core.eh.Result.Err
Represents an error value.
Methods:
isOk(&self) -> bool- Returns true if the result is a Ok value.isErr(&self) -> bool- Returns true if the result is a Err value.ok(self) -> Option<O>- If the result is a Ok value, returns a Some value with the inner value. If the result is a Err value, returns a None value.err(self) -> Option<E>- If the result is a Ok value, returns a None value. If the result is a Err value, returns a Some value with the inner value.expect(self, msg: str) -> O- Returns the inner value of the result. Panics withmsgif the result is a Err value.expectErr(self, msg: str) -> E- Returns the inner value of the result. Panics withmsgif the result is a Ok value.unwrap(self) -> O- Returns the inner value of the result. Panics if the result is a Err value.unwrapErr(self) -> E- Returns the inner value of the result. Panics if the result is a Ok value.unwrapOr(self, backup: O) -> O- Returns the inner value of the result if it is a Ok value. If the result is a Err value, returnsbackup.unwrapOrElse(self, f: fn(E) -> O) -> O- Returns the inner value of the result if it is a Ok value. If the result is a Err value, invokesfwith the inner value and returns the result of the invocation.map<U>(self, f: fn(O) -> U) -> Result<U, E>- If the result is a Ok value, returns a Ok value with the result of invokingfon the inner value. If the result is a Err value, returns a Err value with the inner value. WhereUis the type of the new success value.mapErr<F>(self, f: fn(E) -> F) -> Result<O, F>- If the result is a Ok value, returns a Ok value with the inner value. If the result is a Err value, returns a Err value with the result of invokingfon the inner value. WhereFis the type of the new error value.mapOr<U>(self, backup: U, f: fn(O) -> U) -> U- If the result is a Ok value, returns the result of invokingfon the inner value. If the result is a Err value, returnsbackup. WhereUis the type of the new success value.mapOrElse<U>(self, backup: fn(E) -> U, f: fn(O) -> U) -> U- If the result is a Ok value, returns the result of invokingfon the inner value. If the result is a Err value, returns the result of invokingbackupon the inner value. WhereUis the type of the new success value.and<U>(self, other: Result<U, E>) -> Result<U, E>- If the result is a Ok value, returnsother. If the result is a Err value, returns a Err value with the inner value. WhereUis the type of the new success value.andThen<U>(self, f: fn(O) -> Result<U, E>) -> Result<U, E>- If the result is a Ok value, returns the result of invokingfon the inner value. If the result is a Err value, returns a Err value with the inner value. WhereUis the type of the new success value.or<F>(self, other: Result<O, F>) -> Result<O, F>- If the result is a Ok value, returns the result. If the result is a Err value, returnsother. WhereFis the type of the new error value.orElse<F>(self, f: fn(E) -> Result<O, F>) -> Result<O, F>- If the result is a Ok value, returns the result. If the result is a Err value, returns the result of invokingfon the inner value. WhereFis the type of the new error value.
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:
T- The type of the value.
Methods:
isSome(&self) -> bool- Returns true if the option is a Some value.isNone(&self) -> bool- Returns true if the option is a None value.expect(self, msg: str) -> T- Returns the inner value of the option. Panics withmsgif the option is a None value.unwrap(self) -> T- Returns the inner value of the option. Panics if the option is a None value.unwrapOr(self, backup: T) -> T- Returns the inner value of the option. If the option is a None value, returnsbackup.unwrapOrElse(self, f: fn() -> T) -> T- Returns the inner value of the option. If the option is a None value, invokesfand returns the result of the invocation.map<U>(self, f: fn(T) -> U) -> Option<U>- If the option is a Some value, returns a Some value with the result of invokingfon the inner value. If the option is a None value, returns a None value. WhereUis the type of the new value.mapOr<U>(self, backup: U, f: fn(T) -> U) -> U- If the option is a Some value, returns the result of invokingfon the inner value. If the option is a None value, returnsbackup. WhereUis the type of the new value.mapOrElse<U>(self, backup: fn() -> U, f: fn(T) -> U) -> U- If the option is a Some value, returns the result of invokingfon the inner value. If the option is a None value, returns the result of invokingbackup. WhereUis the type of the new value.okOr<E>(self, err: E) -> Result<T, E>- If the option is a Some value, returns a Ok value with the inner value. If the option is a None value, returns a Err value witherr. WhereEis the type of the error value.okOrElse<E>(self, f: fn() -> E) -> Result<T, E>- If the option is a Some value, returns a Ok value with the inner value. If the option is a None value, returns a Err value with the result of invokingf. WhereEis the type of the error value.and<U>(self, other: Option<U>) -> Option<U>- If the option is a Some value, returnsother. If the option is a None value, returns a None value. WhereUis the type of the new value.andThen<U>(self, f: fn(T) -> Option<U>) -> Option<U>- If the option is a Some value, returns the result of invokingfon the inner value. If the option is a None value, returns a None value. WhereUis the type of the new value.or(self, other: Option<T>) -> Option<T>- If the option is a Some value, returns the option. If the option is a None value, returnsother.orElse(self, f: fn() -> Option<T>) -> Option<T>- If the option is a Some value, returns the option. If the option is a None value, returns the result of invokingf.xor(self, other: Option<T>) -> Option<T>- If the option is a Some value, returns a None value. If the option is a None value, returnsother.filter(self, f: fn(T) -> bool) -> Option<T>- If the option is a Some value, and invokingfon the inner value returns true, returns the option. Otherwise, returns a None value.
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:
source(&self) -> Option<&(dyn Error + 'static)>- Returns the underlying cause of this error, if any. (If the error is a wrapper around another error, this method should return the underlying error.) Should default toNone.
Trait Implementation Requirements:
core.fmt.Debug- All implementors ofErrormust also implementDebug.core.fmt.Display- All implementors ofErrormust also implementDisplay.
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:
msg- The message to display when the panic occurs.
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:
T- The type of the value.