DriftScript
Documentation

Guide

Lists and loops

A list, the loops that walk it, and constants a whole file can name.

A list, the loops that walk it, and constants a whole file can name. All three are language forms rather than a library, which is the decision worth understanding before the syntax: there is no standard collections module to import, and there is not going to be one.

The list is in the language, not at the boundary

A capability parameter type is a name in a data format a host writes, so a module function over a list would need a type variable in that format. Keeping lists as language forms is what lets a script hold and walk a collection without either side of the host boundary learning about generics. A capability may still answer a list, which is what a path from a navigation host is.

// A list, the loops that go with it, and a constant the whole file can name.
//
// `[a, b]`, `xs[i]`, `len`, `push` and `for … in` are language forms rather than a module, which is
// why there is no `std/collections`: a capability's parameter types are names in a data format a
// host writes, and a module function over `List<T>` would need a type variable there.

// A `let` at the top of a file is a constant. Its value is arithmetic over literals and other
// constants; a call is not allowed, because a module is evaluated before its host is bound and
// there would be nothing to call. Another file can import it the way it imports a function.
let CALM = 0.15
// Annotated, because `len` answers a `u32` and there is no implicit widening: a bare `3` here is
// an `f32` and comparing the two is refused by name.
let SAMPLES: u32 = 3

// `continue` skips to the next turn, `break` leaves the loop. There are no labels, so a jump
// always means the loop it is written in.
fn strongestGust(gusts: List<f32>) -> f32 {
    var best: f32 = 0
    for gust in gusts {
        if gust < CALM {
            continue
        }
        if gust > best {
            best = gust
        }
    }
    return best
}

// `push` needs a mutable binding, because growing a list writes to the container.
fn firstFew(gusts: List<f32>) -> List<f32> {
    var picked: List<f32> = []
    for gust in gusts {
        if len(picked) == SAMPLES {
            break
        }
        push(picked, gust)
    }
    return picked
}

// An index past the end throws, for the same reason integer overflow does: there is no value that
// would be right, and returning one would be inventing an answer.
fn opening(gusts: List<f32>) -> f32 {
    if len(gusts) == 0 {
        return 0
    }
    return gusts[0]
}
A list, a loop over it, and a constant the file names once.

An index past the end throws

For the same reason integer overflow does: there is no value that would be right, and returning one would be inventing an answer. Check the length first, which is what len is for, and a list you built yourself needs no check at all.

Growing a list writes to it

push needs a mutable binding, because a container that grows is a container that changed. That is the same rule mut carries everywhere else, applied to the thing being pushed into rather than to what is pushed.

break and continue take no label

break leaves the innermost loop and continue skips to its next turn, and a jump always means the loop it is written in. One case is worth knowing: a break out of a query loop still finishes walking the cursor, because a cursor returns to the pool when the walk reports exhaustion and by no other route, and leaving early would strand one per frame.

A list loop may suspend, and a query loop may not

A task can await inside a for over a list. The loop's position moves onto the task frame, so the walk resumes on the element it stopped at instead of starting again. A query loop refuses, because its cursor comes from a pool and returns to it on exhaustion, and holding one across a frame means walking an array every system since has rewritten.

A list of wolves is not a list of dogs

Lists are invariant, even where the element types are related. A list can be written through, so a covariant one would let a plain dog be pushed into a reference whose real list holds wolves, and every reader of that list believes it holds wolves. Record subtyping is sound here partly because that cannot happen.

// Deliberately refused.
//
// A `List<Wolf>` is not a `List<Dog>`, even though a `Wolf` is a `Dog`. With covariance the call
// below would hand `adopt` a reference whose real list holds wolves, and pushing a plain `Dog` into
// it would put one in a list every reader believes is wolves.
//
// Record subtyping is sound in this language partly because that cannot happen: a record is read
// through its fields and a list can be written through.
//
// `:` before the brace is the base clause. It is not a new keyword, and a reader meets `:` here
// meaning "extends" and three lines later meaning "has type"; the position is what keeps the two
// apart, since a type annotation never follows a record's name.

data Dog {
    weight: f32
}

data Wolf: Dog {
    pack: u32
}

fn adopt(kennel: mut List<Dog>, arrival: Dog) -> u32 {
    push(kennel, arrival)
    return len(kennel)
}

fn release(pack: mut List<Wolf>, stray: Dog) -> u32 {
    return adopt(pack, stray)
}

A constant is arithmetic, never a call

A let at the top of a file is a constant the whole file can name and another file can import. Its value is a number, a string, a bool, or arithmetic over those and other constants. A call is refused, because a module is evaluated before its host is bound and there would be nothing to call. Declaration order carries no meaning, so a cycle between constants is refused naming every constant in it.