Skip to content

Script plugin

A small imperative language for computing over models. Its main job is supplying the objective and constraint functions of an optimisation, but scripts can also be run on their own.

At a glance

Plugin idscript-service
Display nameScript
DescriptionLanguage support for script definitions (the manifest string still says .s files; the extension is .fn)
Default URL/plugin/script
Sourceapp/packages/service-script, app/packages/language-script, app/packages/language-expression
Depends onThe Metamodel plugin; executions go to script-execution

Languages contributed

Language idNameExtensionTextual editorGraphical editorGenerated
scriptScript.fn

The script language

A .fn file optionally names a metamodel, may import functions from other script files, and defines functions.

fn
using "./tasks.mm"

// Total effort of every task that nobody has picked up yet.
fun unassignedEffort(): int {
    var total = 0
    for (task in Task.all()) {
        if (task.assignee == null) {
            total = total + task.effort
        }
    }
    return total
}

// How much the busiest developer is overloaded beyond their capacity.
fun maxOverload(): int {
    var worst = 0
    for (developer in Developer.all()) {
        var assigned = 0
        for (task in developer.tasks) {
            assigned = assigned + task.effort
        }
        var overload = assigned - developer.capacity
        if (overload > worst) {
            worst = overload
        }
    }
    return worst
}

// Constraint: every high priority task has to be assigned.
// 0 means satisfied, any larger value is the magnitude of the violation.
fun unassignedHighPriority(): int {
    var violations = 0
    for (task in Task.all()) {
        if (task.priority == Priority.HIGH && task.assignee == null) {
            violations = violations + 1
        }
    }
    return violations
}

Functions

fn
fun name(parameter: Type, other: Type): ReturnType {
    return value
}

The return type may be omitted for a function that returns nothing, or written as void.

Functions from another file are imported by name, optionally renamed:

fn
import { unassignedEffort, maxOverload as overload } from "./objectives.fn"

Reaching the model

A script that declares using "./tasks.mm" gets one accessor per class in that metamodel: Task.all() returns every Task in the model being evaluated. From there you navigate through the properties and associations the metamodel declares.

fn
for (task in Task.all()) {
    if (task.assignee == null) { }
}

Types

CategoryTypes
Numbersint, long, float, double
Other primitivesstring, boolean
CollectionsCollection<T>, List<T>, Set<T>, Bag<T>, OrderedSet<T>, Iterable<T>
DomainEvery class and enum of the imported metamodel
Lambdas(A, B) => R
Top typeAny

A ? suffix makes a type nullable: Shape?, (Int) => Int?.

Statements and expressions

fn
using "./shapes.mm"

// Literals of every supported kind.
fun literals(): int {
    var integer = 42
    var big = 42L
    var single = 1.5f
    var precise = 1.5
    var text = "with \"escapes\" and \n newlines"
    var flag = true
    var nothing: string? = null
    var numbers = listOf(1, 2, 3)
    println(text)
    return integer
}

// Operators, grouped by precedence from tightest to loosest.
fun operators(a: int, b: int): boolean {
    var arithmetic = a * b / 2 + a - b % 3
    var comparison = arithmetic < 100 && arithmetic >= 0
    var equality = a == b || a != b
    var ternary = comparison ? arithmetic : 0
    return equality && ternary >= 0
}

// Null handling: null-safe access, elvis and the non-null assertion.
fun nullHandling(shape: Shape?): string {
    var maybeName = shape?.name
    var name = maybeName ?? "unnamed"
    if (shape != null) {
        return shape!!.name
    }
    return name
}

// Type checks and casts.
fun area(shape: Shape): double {
    if (shape is Rectangle) {
        var rectangle = shape as Rectangle
        return rectangle.width * rectangle.height
    }
    var circle = shape as? Circle
    if (circle != null) {
        return 3.14159 * circle.radius * circle.radius
    }
    return 0.0
}

// Control flow and collection access through the generated `all()` accessor.
fun visibleShapeCount(): int {
    var count = 0
    for (shape in Shape.all()) {
        if (!shape.visible) {
            continue
        }
        if (shape.colour == Colour.RED) {
            count = count + 2
        } else if (shape.colour == Colour.GREEN) {
            count = count + 1
        } else {
            count = count + 1
        }
    }

    var guard = 0
    while (guard < 10) {
        guard = guard + 1
        if (guard > 5) {
            break
        }
    }
    return count
}

// Lambdas are ordinary expressions and can be passed to collection operations.
fun totalArea(): double {
    var total = 0.0
    for (shape in Shape.all()) {
        total = total + area(shape)
    }
    return total
}

Statements are var declarations, assignments, if / else if / else, while, for … in, break, continue and return. Statements are separated by line breaks, not semicolons.

Operators, from tightest to loosest binding:

GroupOperators
Postfix., ?., !!, calls
Unary!, -
Castas, as?
Multiplicative*, /, %
Additive+, -
Elvis??
Type checkis, !is
Relational<, >, <=, >=
Equality==, !=, ===, !==
Conjunction&&
Disjunction||
Conditional? :

Lambdas are written (a, b) => expression or (a, b) => { … } and are ordinary expressions.

The standard library provides println, the collection constructors listOf, setOf, bagOf, orderedSetOf, and their empty counterparts emptyList, emptySet, emptyBag, emptyOrderedSet.

Objective and constraint functions

A function used as an objective or a constraint in a .config file has to satisfy two rules, enforced by validation in the editor:

  • it takes no parameters — it reads the model through the all() accessors;
  • it returns a numeric type (int, long, float or double).

For a constraint, 0 means satisfied and any larger value is the magnitude of the violation.

Contribution plugins contributed

Target languageWhat it adds
configThe script Function type export

Script functions for the config language

Like the metamodel plugin, the script plugin contributes no syntax to the config language. It exports its Function AST type so that contribution plugins which do add syntax can write rules referencing script functions — which is how minimize unassignedEffort in a goal section resolves to the function in your .fn file.

Contribution plugin idconfig-script
Short namescript
Sectionsnone
Exported typesFunction

Contributions the script language accepts

The script language is itself extensible. A plugin can register a script contribution plugin that adds:

  • functions — extra entries in the global scope, with signatures and an implementation given as a typed AST;
  • expressions — new syntax, backed by a grammar rule and implemented by a function.

Nothing in the bundled set uses this yet, but it is the supported way to grow the standard library without changing the script language.

Server-side capabilities

File data keyContents
astThe serialised AST
typed-astThe type-annotated AST the execution service interprets

Execution. A script function can be run against a model. The plugin forwards the request to the script-execution service, configured through SCRIPT_EXECUTION_SERVICE_URL, which runs it in a sandboxed subprocess under a timeout.

Released under the terms of the repository licence.