Quick Reference

JSON Superset

Ase is a superset of JSON, meaning valid JSON will always be valid Ase.

{
    "title": "All valid JSON is valid Ase!",
    "message": "When in doubt, just use JSON",
    "benefits": [
        "Compatible with existing tools, documentation, and languages",
        "Immediately understandable and easy to use"
    ]
}

To make it easier for humans to read and write, Ase does allow for some differences to JSON. For machines, Ase has an optimized binary format that it gets parsed into.

// Comments are allowed everywhere
title: 'Single quotes are accepted, and outer braces are optional',
message: 'Field names do not need quotation marks',
delimiters: 'Field declarations can end in "," or ";" or nothing at all';
trailing: "Objects can have trailing delimiters ',' or ';'"
trailing_array_comma: [1, 2, 3,] // Arrays can have a trailing comma
positive_sign: +2; // positive 2

/**
 * All of this allows Ase to be quite small and efficient.
 * Here's some JSON.
 */
{"one":1,"two":2,"three":3,"obj":{"four":4,"five":5},"six":6}

/**
 * Here's how it can be represented in Ase.
 */
one:1 two:2 three:3 obj:{four:4 five:5}six:6

Types

/**
 * Comments are always allowed in Ase.
 */
{
    string "title": "Ase adds types at the beginning of field declarations!"
    string 'message': "Ase will cast to these types when the document is parsed"
    example: {
        f64 float_example: "100" // Gets cast to a f64 when it gets parsed
    }, // trailing commas are just fine
}
// No need for the extra braces, we just assume we're in the "root" object scope
string "title": "Can use commas, semicolons, or nothing at all between fields!";
message: "No need for the quotation marks either on field names"
// Value types
null_field: null

object obj_field: { /* More fields */ }
string str_field: 'strings'
bool   bool_field: true

// Number types - can also have 'units', see Number Types section for info
i64    i64_field: 42
i32    i32_field: 42
f64    i64_field: 42.0
f32    f32_field: 42.0

array  array_field: [1, 2, 3]
tuple  tuple_field: (1, 2, 3)
blob   blob_field:  [1, 2, 3] // vec u8

fn     func_field: ():string => 'hello' // anonymous function

Object Types

Objects are the backbone of Ase (just like JSON) and denote the document structure/relationships. Objects in Ase (text format) can contain fields and functions.

The root object is named "root":

// This field is contained by the "root" object
field: 'hello'

// Fields can be referenced via relative paths (self or super)
rel_clone: self.field

// Fields can also be referenced via absolute paths
abs_clone: root.field

Objects have an implicit "object" type.

// Here, we are creating a field and object named "child"
// The "object" type is implied, but here we add it anyway
// "child" is a child object of "root"
object child: {
    string field: 'hello';
    string rel_clone: self.field; // N.B: 'super.child.field' would also get you here
    string abs_clone: root.child.field;
    
    another_child: {
        // This object is a child of "child" named "another_child"
        // You get the pattern...
    }
}

Ase can have many roots. Here's how to create one (from within any object, no matter how deep).

root Variables: {
    // This is an additional root object named "Variables"
    string field: 'hello';
    string rel_clone: self.field;
    string abs_clone: Variables.field; // outside of the 'root' object
}

Referencing/Creating Nested Objects

An object name can create many objects at once, and object "definitions" can exist multiple times.

// Here, we are creating the "other", "additional", "help", and "nested"
// objects all at once.
object other.additional.help.nested: {
    hello: 'Hi'
}

// We can always have additional object declarations, adding to already
// created objects.
other.additional.help.nested: {
    name: 'Bob'
    
    // We can also declare fields on parents by referencing them (crazy, I know)
    // Can create paths by name or with 'super' or 'self' at any time
    // Checks children first, then parent/super, then self
    help.super.self.help2: {
        // Declared a new object named "help2" on the "additional" object
    }
}

// #[init] - This function will get called when this Ase is parsed (at the end).
#[init]
fn initialize() {
    let hi = self.other.additional.help.nested.hello;
    let name = self.other.additional.help.nested.name;
    
    // Creates a field named "message" on self (root object)
    self.message = `${hi}, ${name}!`; // "Hi, Bob!"
}

Creating Objects from Functions

Objects can be created from within functions like so:

/**
 * #[init] - This function will get called when this Ase is parsed (at the end).
 */
#[init]
fn example() {
    // This statement will create a new anonymous object in the document and
    // assign it to a field named "obj" on self (root object).
    self.obj = new {
        example: true;
    };
}

Type Declarations

Ase has type declarations too, but they aren't as developed (yet). Lots more to come in this section. Probably best to use this sparingly - not much reason to use it yet with Formata.

type Point2D {
    x: f64;
    y: f64;
    fn len(): f64 {
        return Number.sqrt(self.x.pow(2) + self.y.pow(2));
    }
}
type Point extends Point2D {
    z: f64;
    fn len(): f64 {
        return Number.sqrt(self.x.pow(2) + self.y.pow(2) + self.z.pow(2));
    }
}

// Casts this object to the "Point" type
Point point: {
    x: 2cm
    y: 1cm
    z: 3cm
}

/// Returns "Length: 3.74cm, Length2D: 2.24cm"
fn main(): string {
    let point = self.point;
    
    // Another valid way to create a Point with "new"
    // This way adds some field validation when parsing
    /*let point = new Point {
        x: 2cm
        y: 1cm
        z: 3cm
    };*/
    
    let len2d = point.super::len().round(2);
    let len = point.len().round(2);
    
    return `Length: ${len}, Length2D: ${len2d}`;
}

Object Standard Library

obj: {
    a: 'A'
    b: 'B'
    c: 'C'
}

// To string - JSON string for the object
to_string: self.obj.toString(); // {"a":"A","b":"B","c":"C"}

// Length - used in default iteration
len: self.obj.len(); // 3 - number of fields in the object

// at - used in default iteration and for indexing into objects
at:  self.obj.at('a'); // 'A'
at1: self.obj['b'];    // 'B' - parses to self.obj.at('b')
ata: self.obj.at('a', 'b'); // array ['A', 'B'] - can pass as many as you'd like
ata1: self.obj['a', 'b'];   // array ['A', 'B'] - can pass as many as you'd like

// Fields
fields: self.obj.fields(); // [('a', 'A'), ('b', 'B'), ('c', 'C')] - array of tuples
keys: self.obj.keys();     // ['a', 'b', 'c']
values: self.obj.values(); // ['A', 'B', 'C']

// Remove a field (without 'drop' syntax) - takes at least one field name
removed: self.obj.remove('a', 'b'); // [('a','A'), ('b','B')] - were removed

// Set a field (without just assigning to a field directly)
set: self.obj.set('a', 'A'); // 'A' - the value set

Creating and Removing Fields

To create fields in functions, just assign them to objects!

#[init] // Means it will be executed automatically after everything has been parsed
fn example() {
    // Assign a field named "example" to the root object with a value of true
    self.example = true;
    
    // Create a sub-object named "child" and create a field on child named "example"
    // If the sub-object already exists, just add/overwrite the field
    self.child.example = true;
    
    // Use the 'set' function - creates a field named 'test' with a value of true
    self.set('test', true);
}

To remove fields, use the "drop" syntax or call "remove".

#[init]
fn remove_example() {
    // Drop syntax
    drop self.example;
    drop self.child.example;
    
    // Remove function
    self.remove('test');
}

Number Types

Ase has 4 number types: f64, f32, i64, and i32. There's technically a 5th, which is an f64 variant that adds units and performs unit conversions during operations and when casting.

Here is some valid Ase, outlining the number types:

number_types: {
    // Default float is an f64 if no explicit type given
    f64 float_64: 42.0
    f32 float_32: 42.0
    
    // Default integer is an i64 if no explicit type given
    i64 int_64: 42
    i32 int_32: 42
    
    /**
     * Unit types are a variant of f64 and can be used anywhere types are accepted.
     * All numbers can always be cast into units, and units can be removed.
     * Performs unit conversions when cast between unit types (within each category).
     * Number literals can also accept units at the end of the number with no spaces.
     */
    unit_types: {
        /**
         * Angle units - clamped between [0deg, 360deg].
         * - Degrees - 'deg' | 'degrees'
         * - Radians - 'rad' | 'radians'
         *
         * Positive angles - always kept positive [0deg, 360deg).
         * - Positive Degrees - 'pdeg' | 'pdegrees'
         * - Positive Radians - 'prad' | 'pradians'
         */
        angles: {
            literal_example: 30deg
            rad type_example: 10deg // cast to radians when parsed
        }
    
        /**
         * Units of Length.
         * - Kilometers -- 'km'  | 'kilometer'  | 'kilometers'
         * - Hectometers - 'hm'  | 'hectometer' | 'hectometers'
         * - Decameters -- 'dcm' | 'decameter'  | 'decameters'
         * - Meters ------ 'm'   | 'meter'      | 'meters'
         * - Decimeters -- 'dm'  | 'decimeter'  | 'decimeters'
         * - Centimeters - 'cm'  | 'centimeter' | 'centimeters'
         * - Millimeters - 'mm'  | 'millimeter' | 'millimeters'
         * - Micrometers - 'um'  | 'micrometer' | 'micrometers'
         * - Nanometers -- 'nm'  | 'nanometer'  | 'nanometers'
         * - Miles ------- 'mi'  | 'mile'       | 'miles'
         * - Yards ------- 'yd'  | 'yard'       | 'yards'
         * - Feet -------- 'ft'  | 'foot'       | 'feet'
         * - Inches ------ 'in'  | 'inch'       | 'inches'
         */
        length: {
            // Declaring fields with types
            literal_example: 42km
            km type_example: 42
            
            // Function example
            // Ase casts/converts units where needed
            // Any number passed in as 'val' will be cast to units of cm
            fn add_2m(val: cm): km {
                return val + 2m;
            }
        }
        
        /**
         * Units of Time.
         * - Days --------- 'day' | 'days'
         * - Hours -------- 'hr'  | 'hrs'         | 'hour'   | 'hours'
         * - Minutes ------ 'min' | 'mins'        | 'minute' | 'minutes'
         * - Seconds ------ 's'   | 'second'      | 'seconds'
         * - Milliseconds - 'ms'  | 'millisecond' | 'milliseconds'
         * - Microseconds - 'us'  | 'microsecond' | 'microseconds'
         * - Nanoseconds -- 'ns'  | 'nanosecond'  | 'nanoseconds'
         */
        time: {
            literal_example: 42days
            hrs type_example: 42
            
            // Function example
            // Takes any f64 (won't add units if not present) and adds 400ms
            fn add_400ms(val: f64): seconds {
                return val + 400ms;
            }
        }
        
        /**
         * Units of Temperature.
         * - Kelvin ----- 'K' | 'kelvin'     | 'Kelvin'
         * - Celsius ---- 'C' | 'celsius'    | 'Celsius'
         * - Fahrenheit - 'F' | 'fahrenheit' | 'Fahrenheit'
         */
        temperature: {
            literal_example: 32F as C, // converts F to C
            K type_example: 273.15,
            
            // Add 2 degrees F to any value and return as C
            fn add_2F(val: f64): C {
                return val + 2F;
            }
        }
        
        /**
         * Units of Mass.
         * - Gigatonnes - 'Gt'  | 'gigatonne' | 'gigatonnes'
         * - Megatonnes - 'Mt'  | 'megatonne' | 'megatonnes'
         * - Tonne ------ 't'   | 'tonne'     | 'tonnes'
         * - Kilograms -- 'kg'  | 'kilogram'  | 'kilograms'
         * - Grams ------ 'g'   | 'gram'      | 'grams'
         * - Milligrams - 'mg'  | 'milligram' | 'milligrams'
         * - Micrograms - 'ug'  | 'microgram' | 'micrograms'
         * - Nanograms -- 'ng'  | 'nanogram'  | 'nanograms'
         * - Picograms -- 'pg'  | 'picogram'  | 'picograms'
         * - Tons (US) -- 'ton' | 'tons'      | 'Ton'        | 'Tons'
         * - Pounds ----- 'lb'  | 'lbs'       | 'pound'      | 'pounds'
         * - Ounces ----- 'oz'  | 'ounce'     | 'ounces'
         */
        mass: {
            literal_example: 129035g as kg;
            kilograms type_example: 42;
        }
    }
}

Number Standard Library

Number library functions can be called on any number type. Can call the function as demonstrated below, but can also reference the library by name for all functions, passing the number value as the first parameter (Ex. Number.pow(val, 2)).

If a number has units, the library functions will preserve the units of the value passed in. For functions with additional number parameters (ex. pow), units for the additional numbers are ignored. If needed, cast this to the desired f64 value before executing.

// Remove units function
unitless_f64: 2m.removeUnits(); // 2

// Units function (same result as 'typeof x')
units: 2m.units(); // 'm'

// Has units?
has_units: 2m.hasUnits(); // true

// Is angle units?
is_angle: 30deg.isAngle(); // true
is_degrees: 20deg.isDegrees(); // true
is_radians: 20deg.isRadians(); // false

// Positive angle types? Always kept positive.
// Useful for comparisons and angle subtraction
is_positive_degrees: 30pdeg.isPositiveDegrees(); // true
is_positive_radians: 3.141592prad.isPositiveRadians(); // true

// Is temperature units?
is_temp: 2C.isTemp(); // true

// Is length units?
is_len: 2kg.isLength(); // false

// Is time units?
is_time: 2s.isTime(); // true

// Is mass units?
is_mass: 2kg.isMass(); // true

// Square root
sqrt: 144kg.sqrt(); // 12.0kg

// Cubed root
cbrt: 222.cbrt();

// Absolute value
abs: -34.abs(); // 34

// Power
pow: 2.pow(2); // 2^2

// Floor function
floor: 23.3.floor(); // 23

// Ceil function
ceil: 23.3.ceil(); // 24

// Round function
rounded:   34.6.round();    // 35
rounded_2: 34.49.round(1);  // 34.5
rounded_3: 34.337.round(2); // 34.34

// Truncate function - returns integer part of number, towards 0
trunc:  -3.7.trunc(); // -3.0
trunc_1: 3.7.trunc(); //  3.0

// Fractional function - returns fractional part of number
fract:    3.6.fract(); //  0.6
fract_1: -3.6.fract(); // -0.6

// Sign number function
sign:    32.signum(); //  1.0
sign_1: -32.signum(); // -1.0

// Exp function e^(self)
exp: 3.exp(); // e^3

// Exp2 function 2^(self)
exp2: 3.exp2(); // 2^3

// Natural log ln(self)
ln: 1.ln(); // ln(1)

// log with base
log: 8.log(2); // log2(8)

// Trig - treated as radians if without units of degrees
sin: 90deg.sin(); // 1
cos: 0deg.cos();  // 1
tan: 0deg.tan();  // 0

// Arcsine in radians (-pi/2 to pi/2)
asin: self.sin.asin(); // 90deg (in radians with units)

// Arccosine in radians (0 to pi)
acos: self.cos.acos(); // 0rad (in radians with units)

// Arctan in radians (-pi/2 to pi/2)
atan: self.tan.atan(); // 0rad (in radians with units)

// Atan2 - 4 quadrant arctangent between self (y) and other (x) in radians
atan2: 1.atan2(0); // 90deg (in radians with units)

// TrigH in radians
sinh: 3.1416.sinh();
cosh: 3.1416.cosh();
tanh: 3.1416.tanh();
asinh: 3.1416.asinh();
acosh: 3.1416.acosh();
atanh: 3.1416.atanh();

String Type

hello: 'Hello'
string world: "world"

// Returns "Hello, world!"
fn sayHello(): string {
    return `${self.hello}, ${self.world}!`;
}

String Standard Library

// Length
len: "hello".len(); // 5

// At
at: "hello".at(1); // "e"
at1: { let v = 'hello'; return v[1]; }; // "e"

// First and last
first: 'first'.first(); // 'f'
last: 'first'.last();   // 't'

// Starts and ends with
starts_with: 'first'.startsWith('f'); // true
ends_with: 'first'.endsWith('with');  // true

// Push
push: 'hello'.push(', world'); // 'hello, world'
add:  'hello' + ', world';     // 'hello, world'

// Contains
contains: 'hello, world'.contains('world'); // true

// Index of (index of the first char if found or -1 if not found)
index_of: 'hello, world'.indexOf('world'); // 7

// Replace all instances with another string
replace: 'hello world'.replace(' ', ', '); // 'hello, world'

// Split
split: '1,2,3'.split(','); // ['1', '2', '3']

// Substring
substr: 'hello, world'.substring(7);     // 'world'
substr1: 'hello, world'.substring(7, 9); // 'wo'

// To uppercase
upper: 'hello'.toUpper(); // 'HELLO'
lower: 'HELLO'.toLower(); // 'hello'

// Trim whitespace and newlines
trim:       '   hello   '.trim();        // 'hello'
trim_start: '   hello   '.trimStart();   // 'hello   '
trim_end:   '   hello   '.trimEnd();     // '   hello'

Functions

Functions in Ase can manipulate the document in which they are defined, even if that data doesn't exist yet (comes from another document). The runtime portion of Ase is very minimal, acting as a sandboxed wrapper for quick execution at any time. If the function cannot run, you'll get an error at the time of calling it.

Functions are completely separate from fields - they are two independent data types. Therefore, names never collide, etc... However, Ase has a function pointer field value type fn that can point to functions anywhere in the document. When parsed, these functions need to exist! Ase parses top down.

Being an Ase-only concept, functions are not exported when Ase is turned into JSON, YAML, TOML, etc... This is a part of the "upgrading" to Ase and "downgrading" from Ase that was mentioned on the "Get Started" page. After "upgrading", you can always add functions or call functions on the data.

fn init() {
    // Do some stuff with this document
}

fn function_field: self.init // must already be defined in the document

fn call_init_with_field() {
    // Call other functions by referencing their object first
    // In this case, this func is in the same scope as 'init' - we can use 'self'
    // Because this is also the 'root' object, we could also use 'root.init();'
    self.init();

    // Can call function pointers/fields with the 'call' function
    self.function_field.call();
    
    // Another way is to reference the Function lib
    Function.call(self.function_field);
    
    // Can create arrow functions to call in the same way
    let func = (): string => 'hi';
    let func1 = (): string => { return ' there'; };
    let res = func.call() + func1.call(); // 'hi there'
}

// Functions can be passed around with the 'fn' type
fn call_function(func: fn, a: f64, b: f64) {
    func.call(a, b);
}

// Attribute that also creates a field pointing to the function with the same name
#[field]
fn my_func_field() { /* Is a field and a func. */ }

// Attribute that automatically calls this function after the document is parsed
// These functions get called in the order that they are seen
#[init]
fn call_when_parsed() { /* Gets called after document is parsed. */ }

Array Type

// Array fields
array_field: [1, 2, 3]

// Range field "start..end|step", where step is optional
range_field: 0..4 // [0, 1, 2, 3]
to_ten: 0..10|2   // [0, 2, 4, 6, 8]

fn iteration() {
    for (i in 10) { /* Treats 10 as the 'end' of a range. */ }
    for (i in self.to_ten) { /* i is 0, then 2, then 4 ... */ }
    
    for (let i = 0; i < self.to_ten.len(); i += 1) {
        // i is 0, then 1, then 2, then 3, etc...
        let val = self.to_ten[i];
        // val is 0, then 2, then 4, etc...
    }
    
    // Arrays are not typed
    let hi = '';
    for (val in ['hi', true, 42]) {
        if (val == 'hi') hi = val;
        else if (val == 42) { /* something.. */ }
        else { /* something with true */ }
        
        // The 'for in' syntax expands into a 'while' loop with some extras
        if (first) { /* This is the first value of the array */ }
        if (last)  { /* This is the last value of the array */ }
        
        // 'index' is the index of 'val' in the array
        let idx = index;
    }
}

Array Standard Library

fn array_lib() {
    let array = [1, 2];
    
    // Length (assertions are built-in runtime functions)
    assertEq(array.len(), 2);
    assert(array.any()); // length > 0
    assertEq(array.empty(), false);
    
    // Push
    array.push(3); // void
    array.push(3, 3, 3); // [1, 2, 3, 3, 3, 3]
    
    // Pop
    array.pop(); // 3
    array.pop(0); // 1, array = [2, 3, 3, 3]
    
    // Pop by value (uses 'find')
    array.push('hi'); // array = [2, 3, 3, 3, 'hi']
    array.pop('hi'); // 'hi', array = [2, 3, 3, 3]
    
    // Reverse
    let rev = array.reverse(); // array = [2, 3, 3, 3], rev = [3, 3, 3, 2]
    {
        let ref = ref array;
        ref.reverse(); // array = [3, 3, 3, 2]
        array = deref array; // array was turned into a ref, this undoes that
    }
    
    // At (indexing) - returns a ref!
    let fourth = array.at(3); // or array[3] = 2
    fourth = 4; // array = [3, 3, 3, 4]
    
    let last = array.last(); // Same ref as 'fourth'
    let last_val = deref last; // 4
    let first = array.first(); // ref to first value
    
    first = 5;
    last = 2; // array = [5, 3, 3, 2], fourth = 2, last = 2, last_val = 4
    assertEq(array, [5, 3, 3, 2]);
    assertEq(fourth, 2);
    assertEq(last, 2);
    assertEq(last_val, 4);
    
    // Join
    let joined = array.join(',');
    assertEq(joined, '5,3,3,2');
    
    // Find - returns the index of first match (equals)
    {
        let array = ['a', true, 32, 'str', 'dude'];
        let index = array.find('str');
        assertEq(index, 3);
        assertEq(array.find('dne'), -1);
    }
    
    // Remove - removes the first match (equals)
    {
        let array = ['a', true, 32, 'str', 'dude'];
        let val = array.remove('str');
        assertEq(val, 'str');
        assertEq(array, ['a', true, 32, 'dude']);
    }
    
    // Remove all - removes all matches (equals)
    {
        let array = ['a', 'str', true, 32, 'str', 'dude'];
        let removed = array.removeAll('str');
        assertEq(removed, true);
        assertEq(array, ['a', true, 32, 'dude']);
    }
    
    // Insert values at an index
    {
        let array = [0, 1];
        array.insert(1, 'hi', 'yo');
        assertEq(array, [0, 'hi', 'yo', 1]);
    }
}

Last updated