Modules vs. scripts

SCL source comes in two flavours that look deceptively similar but are compiled by two different front ends and obey different rules:

  • an SCL module (see 1.16 Module system) is a sequence of declarations. It is compiled as a single unit, it defines names for other code to use, and nothing in it runs when it is compiled or imported.
  • an SCL script (see 1.17 SCL Scripts) is a sequence of statements. It is compiled and executed one statement group at a time, top to bottom, exactly as if the statements had been typed into the SCL Console one after another.

The SCL Console is a script that you type interactively. Everything this page says about scripts also holds for the console, with the two exceptions noted under Relative imports and Session lifetime.

This page lists, exhaustively, what each side accepts and what it does not.

The essential difference

Module Script / Console
A list of declarations A list of statements
One compilation unit One compilation unit per statement group
Order does not matter Order is everything
Importing it runs nothing Running it runs everything
Exports names Exports nothing

A module is a definition of a vocabulary. A script is a transcript of a session. Almost every difference below follows from that one distinction.

What may appear at the top level

The script grammar accepts exactly two things at the top level: a statement and an import. Every other declaration form is module-only. When you hit this, the compiler tells you so by listing the tokens it was willing to accept:

> data Color = Red | Green
Unexpected token 'data' (DATA). Expected one of ATTACHED_HASH, BEGIN_STRING,
BLANK, CHAR, CHR_SELECT, CONSTRAINT, DO, EDO, ENFORCE, EQ, ESCAPED_SYMBOL, FLOAT,
ID, IF, IMPORT, INCLUDE, INTEGER, LAMBDA, LAMBDA_MATCH, LBRACKET, LET, LPAREN,
MATCH, MDO, MINUS, SELECT, SELECT_DISTINCT, SELECT_FIRST, TRANSFORMATION, WHEN.

Declaration forms

Declaration Module Script Notes
module { ... } header Yes No module is only a keyword while compiling a module; in a script module { ... } parses as a record constructor and fails with Couldn't resolve the record constructor module
f x = ..., x = ... (value definition) Yes Yes different scoping rules, see Definitions in scripts
f :: Type (type annotation) Yes No in a script this parses as the expression f annotated with a type, and fails with Couldn't resolve f
data T = ... Yes No syntax error
type T = ... (type alias) Yes No syntax error
class C a where ... Yes No syntax error
instance C T where ... Yes No syntax error
deriving instance C T Yes No syntax error
"""documentation""" Yes No in a script this is an ordinary string literal; it is evaluated and printed, and attaches to nothing
@annotation (e.g. @private, @inline) Yes No syntax error
infix / infixl / infixr Yes No syntax error; a script cannot introduce or change operator precedences
import / include Yes Yes see Imports
importJava "..." where ... Yes No syntax error; a script cannot import anything from Java directly
effect E "..." "..." Yes No syntax error
rule / abstract rule ... where ... Yes No abstract rule is a syntax error; the bare word rule crashes the lexer, see Words that break scripts
mapping relation R ... Yes No syntax error
head <-- { ... } (relation definition) Yes No at script top level <-- does not resolve
ruleset R where ... Yes No the word ruleset crashes the lexer, see Words that break scripts

If you need any of these in a script, put them in a module and import that module.

Statement forms

A script's top level is a block, so it accepts syntactically the same statements as a do, let or where block. Several of them are nevertheless unusable there, because the surrounding context a script provides is not the one they need.

Statement Works in a script? Notes
expr Yes evaluated, and its value printed unless it is ()
x = expr, f x y = expr Yes becomes a session variable, see below
f x = expr where ... Yes where attaches to the definition as usual
pattern = expr (e.g. (a, b) = t) Yes every variable the pattern binds becomes a session variable; see Destructuring bindings
x <- expr No at the top level only; inside a do block it works normally. See Monadic bind does not work at the top level
head <-- { query } No <-- does not resolve outside a module-level relation or ruleset
q => q (CHR rule) No parses and type checks, then crashes during code generation
when ... then ... (CHR rule) No as above
constraint C ... No crashes with UnsupportedOperationException
include R expr (CHR ruleset include) No a script cannot compile a CHR block at all

CHR is a module-only feature in practice: rules written in a script get as far as type checking and then fail in the code generator, because a CHR block compiles to a runtime class that belongs to the enclosing module. See 4.02 Constraint Handling Rules.

In practice a script has exactly four usable statement forms: an expression, a value definition, a function definition and a destructuring binding — plus import.

Automatically imported modules

This is the difference that surprises people most often, because it makes scripts look more capable than modules.

Automatically in scope
Module Builtin, Prelude
Script / Console Builtin, StandardLibrary, Expressions/Context

StandardLibrary re-exports Prelude and, on top of it, a large set of commonly used modules — at the time of writing Random, BigInteger, ArrayList (as ArrayList), String (as String), Vector, Databoard, Debug (as Debug), Lazy (as Lazy), File (as File), Serialization (as Serialization), Set (as Set), SetClasses, MMap, MSet, MList, MMultiMap (each under its own name), Coercion, IterN (as Extra), SList (as SList), Arbitrary, Java/Collection (as JC) and Unification. Note that Map is not among them.

So this works in a script:

> Set.fromList [1, 2, 3]
{3, 2, 1}

and the same line in a module fails with Couldn't resolve Set.fromList until you add

import "Set" as Set

When you move code from a script into a module, expect to add imports, even for things that appeared to need none.

Functions that exist only in a script

The command session installs a handful of functions that are not part of any module and that a module can therefore never call. They operate on the session itself.

Function Type Meaning
runFromFile String -> <Proc> () executes another script file in the current session
runTest String -> <Proc> () executes a test script (.sts, with > prompts and expected output)
reset () -> <Proc> () drops session variables and non-persistent imports, rechecks module sources
variables <Proc> [String] names of the current session variables
echoCommands Boolean -> <Proc> () turn the echoing of executed commands on or off
echoingCommands <Proc> Boolean whether commands are currently echoed
startPrintingToFile String -> <Proc> () tee session output into a file
startAppendingToFile String -> <Proc> () as above, appending
stopPrintingToFile <Proc> () stop it

variables, echoingCommands and stopPrintingToFile take no argument at all; reset takes ().

While a script is being run with runFromFile, two extra session variables of type Maybe Files.Path exist:

Variable Value
__SCRIPT_PATH__ absolute path of the script file being run
__SCRIPT_DIR__ the directory containing it

They are restored to their previous values when that script finishes, so a nested script sees its own path, not its caller's. They do not exist when a script is run from the Model Browser, and they never exist in a module.

Definitions in scripts

A definition in a module becomes a compiled constant of the module. A definition in a script becomes a session variable: an entry in a name-to-value map belonging to the command session. That has several consequences that module definitions do not share.

Statements are compiled in groups, and the grouping is positional

The session accumulates consecutive function definitions (a left-hand side with at least one argument) into one compilation unit. Anything else — a value binding without arguments, a bare expression, or an import — ends the group and is compiled on its own.

Within one group, a definition may refer forward to a later one:

> a x = b x
  b x = x + 1
> a 1
2

Across groups it may not, because the earlier group is compiled as soon as the group ends. In this script

isEven n = if n == 0 then True else isOdd (n-1)
sep = 1
isOdd n = if n == 0 then False else isEven (n-1)

the value binding sep = 1 closes the group containing isEven, so isEven is compiled on its own and the script fails on its very first line:

> isEven n = if n == 0 then True else isOdd (n-1)
Couldn't resolve isOdd.

In a module, the same three declarations compile without complaint, because a module has no notion of position. If a script needs helpers that call each other, keep the definitions adjacent — but note that if they call each other in a cycle, the compiler crashes rather than compiling them, see Known compiler defects.

Pattern-matching clauses must be adjacent

In a module, all clauses of a function are collected by name no matter where they appear:

fib 0 = 1
g x = 2
fib n = 99

gives a two-clause fib, so fib 0 is 1 and fib 5 is 99.

In a script, the second fib is a new definition that replaces the first. There is no warning:

> fib 0 = 1
> 1
1
> fib n = 2
> fib 0
2

Adjacent clauses are fine, because they land in the same group:

> fib 0 = 1
  fib 1 = 1
  fib n = fib (n-1) + fib (n-2)
> fib 10
89

Destructuring bindings

A statement whose left-hand side is a pattern binds every variable in that pattern, each as a session variable of its own:

> (a, b) = (1, 2)
> variables
["a", "b"]
> a + b
3

Nested patterns, list patterns, constructor patterns, @ patterns and inline type annotations all work, and _ binds nothing:

> ((c, d), e) = ((10, 20), 30)
> [f, g] = [7, 8]
> Just h = Just "hi"
> i@(j, _) = (5, 6)
> (k :: Integer, l) = (1, "x")

So do record patterns, in all three of their forms. Given the Person record of The module header, and what a script loses with it below:

> Person { name = who, age = years } = p
> Person { name } = p
> Person { name = who, .. } = p

The second binds name, the third binds who and then age — a .. wildcard stands for exactly the fields the pattern does not mention. Note that this is the record pattern syntax, which a script may use; the record.field access syntax still needs the fields feature and is module-only.

The match happens when the statement runs, so a pattern that does not fit its value fails then, like any other run-time error, and binds nothing:

> Just z = Nothing
org.simantics.scl.runtime.exceptions.MatchingException: Matching failure ...

A <- binding inside a do block is different: its variable is local to that block and never reaches the session.

Here it is the module that is the more restricted of the two. A pattern binding at module top level is rejected with Illegal left hand side of the definition; only a let or where block inside a module accepts one.

Session variables are resolved late

A reference to a session variable compiles into a lookup by name, performed when the code runs, not when it is compiled. Redefining the variable therefore changes what already compiled functions see:

> base = 10
> addBase y = base + y
> addBase 5
15
> base = 100
> addBase 5
105

The type, however, was fixed when addBase was compiled and is not rechecked. A redefinition at an incompatible type is accepted and then fails at run time:

> base = 10
> addBase y = base + y
> base = "oops"
> addBase 5
org.simantics.scl.runtime.function.CalledWithTooManyParameters

Redefinition is legal in a script (including shadowing Prelude names — map = 5 is fine) and is the normal way to work in the console. In a module, definitions are fixed once.

No type signatures, and no class-polymorphic definitions

A script cannot write a top-level :: signature, and this is not merely cosmetic. A definition is generalised over ordinary type variables:

> e = []
> ["a"] + e
["a"]
> [1] + e
[1]

> f x = x
> f 1
1
> f "a"
"a"

but it is not generalised over type class constraints. The constraint is resolved when the definition itself is compiled, and the variable is stuck with the resulting type — here Integer -> Integer -> Integer, whatever it is later applied to:

> add x y = x + y
> add 1 2
3
> add 1.5 2.5
Cannot convert real literal to Integer.

A module without a signature behaves the same way, but a module can add the signature and get the polymorphic definition it wants:

add :: Additive a => a -> a -> a
add x y = x + y

A script has no way to do this. Annotating the right-hand side does not help — a context in an inline annotation is rejected outright:

> add = (\x y -> x + y) :: Additive a => a -> a -> a
There is no instance for <Additive a>.

Annotating a concrete type does work, and is the usual workaround when you only need one instantiation:

> add x y = (x + y) :: Double
> add 1.5 2.5
4.0

If you need a genuinely class-polymorphic definition, it belongs in a module.

Monadic bind does not work at the top level

A script statement group is closed off with a () result, so x <- expr at the top level always fails to type check, whatever the monad:

> x <- Just 5
Expected <Maybe a> got <()>.

> x <- [1,2,3]
Expected <[a]> got <()>.

Use <- inside a do/mdo block, where it works normally. For procedural (<Proc>) code you do not need it at all — effects are performed where they are written, so an ordinary x = someProcedure arg binding already runs the effect and binds its result.

Imports

import and include, with as, (...) and hiding (...), are accepted in both modules and scripts and mean the same thing — with these differences.

include is the same as import in a script

include differs from import only by re-exporting. A script exports nothing, so in a script the two are interchangeable.

Position matters in a script

An import in a script affects only the statements that follow it. An import anywhere in a module affects the whole module. This script

greet
import "MyModule"
greet

fails on its first line and never reaches the rest:

> greet
Couldn't resolve greet.

A failing import is fatal only for a module

If an imported module does not compile, a module that imports it does not compile either. A script reports the failure, disables that import, and carries on:

> print "before import"
before import
> import "Broken"
Failed to import Broken, because it contains compilation errors.
    Couldn't resolve nosuchfunction.
> print "after failed import"
after failed import

Relative imports

Relative module names (import "./Utils", import "../Common/Types") are resolved against the importer's own path. That requires knowing what the importer is:

Relative imports
Module Yes, resolved against the module's name
SCL Script in the model Yes, resolved against the script's URI
SCL Console No — the console has no path to resolve against
Script run with runFromFile No — the session's resolution name is unchanged

Cyclic imports

Modules may not form an import cycle; the compiler reports Cyclic module dependency detected. A script cannot be imported at all, so it can never take part in a cycle.

The module header, and what a script loses with it

A script has no place to put a module header, so none of the header's fields are available to it:

Header field Effect Available to a script
features = [fields] enables record.field access syntax No
features = [edo] enables edo blocks No
features = [chr] enables ruleset declarations and CHR select No
export = [...] restricts what the module exports not applicable
defaultLocalName = "..." default prefix for importers not applicable
deprecated / deprecated = "..." marks the module deprecated not applicable
bundle = "..." picks the class loader for importJava not applicable

The fields case is the one that bites. Given a record declared in a module that enables the feature:

module {
    features = [fields]
}

data Person = Person { name :: String, age :: Integer }

p = Person { name = "Ann", age = 30 }
pname r = r.name

a script can call pname p but cannot write p.name itself — without the fields feature, . is not record field access but Simantics variable child browsing, which needs the ReadGraph effect and does not see the record's fields. Write an accessor function in the module and call that from the script.

Words that break scripts

Five identifiers are keywords or not depending on the module header, and the script front end has no header to consult. Any occurrence of one of them anywhere in a script or console command aborts the whole execution with a NullPointerException from the lexer:

edo    rule    ruleset    select    transformation

They cannot be used as variable names, and the constructs they introduce cannot be used either. In particular select ... where { ... } queries cannot be written in a script — wrap the query in a module function and call it.

Evaluation and effects

A script statement is executed the moment it is compiled, in the order written. Effects happen exactly where you wrote them.

A module's top-level values are computed on first use and then memoised. Importing a module runs nothing. Given a module Noisy containing

x = print "MODULE TOP LEVEL RAN"

a script sees this:

> import "Noisy"
> print "before"
before
> x
MODULE TOP LEVEL RAN
> x

The import printed nothing, the first reference to x performed the effect, and the second reference printed nothing because the value had already been computed. Do not use a module-level binding as a way to run something; put the effect in a function and call it.

Both modules and scripts may define effectful values; neither restricts the effect.

Error handling

On an error
Module the module fails as a whole; nothing in it is usable
Script statements before the failure have already run and their effects stand; execution stops at the first failing statement and the rest of the script is not run

This applies to compilation errors and to exceptions alike. The script

print "one"
nosuchthing
print "three"

produces

> print "one"
one
> nosuchthing
Couldn't resolve nosuchthing.

and print "three" is never reached. Note that the error is not detected before the script starts running: print "one" has already happened. A script is therefore not atomic — a partially executed script can leave a half-configured model behind.

Output

A script echoes each command it executes and prints the value of each statement using show. A statement whose value is () prints nothing, and a value whose type has no Show instance prints as a placeholder:

> take 0 []
<value of type [a]>

A module prints nothing at all when it is compiled or imported.

Session lifetime

Session variables and the imports a script performed live in the command session, not in the script. The SCL Console keeps one session for as long as the view is open, so definitions accumulate there. Running an SCL Script from the Model Browser creates a new command session for that run, so a script never inherits the console's variables, and its own definitions do not leak back into the console. runFromFile, by contrast, runs in the current session and does share it.

Choosing between them

Use a module when the code defines things: types, classes, instances, reusable functions, Java imports, relations, operator precedences, documentation. Use a script when the code does things once: configuring a model, running a batch of edits, driving a sequence of operations.

The practical pattern is to keep everything of substance in modules and let the script be a short list of calls into them. That also sidesteps every limitation above.

Moving a script into a module

In the script In the module
implicit StandardLibrary add explicit imports for Set, String, File, Debug, ArrayList, ...
bare statements wrap them in a function, e.g. main = do ...
runFromFile, variables, reset, ... not available; drop them
__SCRIPT_DIR__ pass the directory in as a parameter
destructuring bindings not allowed at top level; bind them inside a function
adjacency-sensitive definitions order no longer matters
monomorphic definitions add :: signatures to make them polymorphic

Moving a module into a script

Mostly you cannot: data, type, class, instance, importJava, effect, infixl, annotations and documentation strings have no script equivalent. Keep them in a module and import it.

Known compiler defects

The following are defects rather than design decisions, and may be fixed in a later release. They are listed here because the failure modes are confusing — each one is a crash rather than a diagnostic. They are tracked as issue #1426.

Trigger Symptom
a cycle of two or more definitions that call each other InternalCompilerError: ... variable <name> was not bounded
the words edo, rule, ruleset, select, transformation NullPointerException from SCLLexer.supportEDO / supportCHR
constraint ... at the top level UnsupportedOperationException from ConstraintStatement.mayBeRecursive

The mutual recursion defect is worth spelling out, because it is wider than the script top level. It affects every mutually recursive binding group evaluated outside a module — at the script top level, in a let block, and in a where block:

> p x = q x
  q x = if x == 0 then 0 else p (x-1)
InternalCompilerError: ... variable p was not bounded.

> let
      p x = q x
      q x = if x == 0 then 0 else p (x-1)
  in p 3
InternalCompilerError: ... variable p was not bounded.

Self-recursion is unaffected; only cycles of length two or more fail. All three forms compile and run correctly inside a module, so the workaround is always the same: move the mutually recursive definitions into a module and call them from the script.

Because the SCL Console, SCL Scripts and embedded SCL expressions share the same evaluator, all three defects apply equally to all of them.