Skip to content

SyncScript

SyncScript is the language a .hyl file is written in. It is a general-purpose language, but it was designed with one goal in mind: hosting internal DSLs which read like a notation of their own. Everything that looks like special diagram syntax - classDiagram { … }, layout { … }, A --> B with { … } - is an ordinary function call in this language.

HyLiMo
classDiagram {
    class("Movie")
}

The one thing to remember

SyncScript has no keywords. if, while, class and even + are variables which happen to hold functions, and every construct is built out of function calls, blocks and objects. Once that clicks, the rest of the language is small.

PropertyWhat it means here
Dynamically typedValues carry their type, variables do not
Strongly typedNo implicit conversions between unrelated types
Statically/lexically scopedA function sees the scope it was written in
Prototype-basedObjects inherit from a prototype, like in JavaScript
Expression-orientedEverything evaluates to a value, including blocks and control flow

The available data types are object, string, number, boolean, function and null.

Syntax

The syntax is inspired by JavaScript, Kotlin and Scala.

Literals

Number and string literals are written as in JavaScript:

HyLiMo
1
3.14
"Hello World"

Strings support the escape sequences \\, \", \n, \t and \uXXXX, and can embed expressions:

HyLiMo
name = "World"
greeting = "Hello ${name}!"

Comments

C-style comments are supported:

HyLiMo
// This is a line-end comment
/* This is a block comment */

Identifiers

As there are no keywords, identifiers are the most important tokens, and there are three kinds of them.

Alphanumeric identifiers contain letters, digits, the underscore and the dollar sign, and do not start with a digit:

HyLiMo
test
test2
hello_world
$variable

Symbolic identifiers are sequences of symbols out of !#%&*+-/:<=>?@^|~.. They are what makes operators possible: an operator is nothing but an identifier.

HyLiMo
+
==
!=
...

Two limitations exist: a single equal sign is not allowed, as it is the assignment operator, and a single dot is not allowed either, as it is the access operator - at least two consecutive dots are needed. Underscores and dollar signs may be used, but must not be followed by an alphanumeric character:

HyLiMo
// allowed
__>
// not allowed
-_test

Escaped identifiers are wrapped in backticks and may contain anything except a newline or a backtick, which allows names which would otherwise be impossible:

HyLiMo
`my identifier` = 42

Because alphanumeric and symbolic identifiers use disjoint character sets, they need no separator: a+b and a + b are the same expression.

Fields

Fields are accessed with the dot operator and assigned with the assignment operator:

HyLiMo
hello.world
theAnswer = 42
hello.world = "Hello World"

Functions

A function is written as a block of expressions in curly braces, and evaluates to the value of its last expression:

HyLiMo
testFunction = {
    1
    2
    3 // the return value of the function
}

Calling a function uses the call operator, with positional and - as in Kotlin - named arguments:

HyLiMo
testFunction()
testFunction(1, 2, 3)
testFunction(a = 1, b = 2, c = 3)

Inside the function, all arguments are available as a single object called args:

HyLiMo
createPoint = {
    x = args.x
    y = args.y
}

The first positional argument is also available as it, and further positional arguments can be taken apart with a destructuring expression:

HyLiMo
printWrapper = {
    println(it)
}

printAll = {
    (a, b, c) = args
    println(a)
    println(b)
    println(c)
}
printAll(1, 2, 3)

Two more names are always available: this is the current scope as an object, and self is the object a function was called on - for a call like point.translate() that is point, and for the call of a plain variable it is the current scope.

HyLiMo
println(this.x)

Trailing lambdas

If the last argument is a function, it can be written after the parentheses - this is what makes the diagram DSL readable:

HyLiMo
testFunction("test") {
    // body of the function
}
// is equivalent to
testFunction("test", {
    // body of the function
})

Unlike Kotlin, several trailing lambdas are allowed, which is how two-branch control flow works without any special syntax:

HyLiMo
if(condition) {
    // if branch
} {
    // else branch
}
// is equivalent to
if(condition, {
    // if branch
}, {
    // else branch
})

Operators

Syntactically, an operator is just an identifier, and at runtime it is resolved to a function. Field access expressions may be used as operators as well:

HyLiMo
a + b
// is equivalent to
+(a, b)

// field access as operator
a this.+ b

For flexibility, the global operators delegate to an implementation on their left-hand side operand, so a type can define what an operator means for it:

HyLiMo
+ = {
    (left, right) = args
    left.+(right)
}

// with this implementation, all of these are equivalent:
a + b
+(a, b)
a.+(b)

No operator precedence

As operators are ordinary functions, there is no precedence: expressions are evaluated strictly from left to right. Use brackets whenever the order matters.

HyLiMo
a + b * c
// is equivalent to
(a + b) * c
// to get the expected result, use brackets:
a + (b * c)

Objects

Objects are created with square brackets. Entries without a name are assigned to the next free index, exactly like positional arguments:

HyLiMo
point = [
    x = 1
    y = 2
]

test = [
    0, // index 0
    x = 1
    2, // index 1
    y = 3
    4 // index 2
]

Fields are read and written with the access operator - point.x - or with get and set if the name is computed.

Standard library

Operators by type

TypeOperators
string==, !=, <, <=, >, >=, + (concatenation, the right side may be any value)
number==, !=, <, <=, >, >=, +, -, *, /, %
boolean==, !=, &&, || (short circuiting), &, |
object==, !=
null==, !=

The ?? operator works on any value: it returns its left side, or its right side if the left side is null. The right side is only evaluated if it is needed.

Object functions

FunctionDescription
getReads the field with the given name
rawGetLike get, but does not consider the prototype chain
setSets the field with the given name to the given value
definePropertyDefines a field with a custom getter and setter
deleteDeletes the field with the given name
forEachCalls the given function for each field, with the value and the name as arguments
toStringConverts the object to a string

Function functions

FunctionDescription
callWithScopeCalls the function with a given scope object - this is what makes DSL blocks such as layout { … } possible

Lists

A list is created with the list function, taking any number of positional arguments:

HyLiMo
numbers = list(1, 2, 3)
MemberDescription
lengthField holding the current length, should not be modified
+Concatenates two lists
+=Appends all elements of another list in place
addAppends one element
addAllAppends all elements of another list
removeRemoves and returns the last element
forEachCalls the given function for each entry, with the value and the index
mapLike forEach, but collects the results into a new list
filterReturns a new list with the entries for which the given function returns true
someReturns whether the given function returns true for at least one entry
joinJoins the entries into a string, with an optional separator
toListConverts the index-based fields of an object into a list

Math

Math provides the usual numerical functions: abs, sign, floor, ceil, round, trunc, min, max, pow, sqrt, cbrt, hypot, exp, expm1, log, log2, log10, log1p, the trigonometric functions sin, cos, tan, asin, acos, atan, atan2 and their hyperbolic counterparts, as well as the constants PI, E, LN2, LN10, LOG2E, LOG10E, SQRT2 and SQRT1_2.

Global functions

FunctionDescription
ifTakes a condition and one or two functions, and calls the first if the condition is true, otherwise the second
whileTakes a condition function and a body function, and calls the body as long as the condition returns true
rangeReturns a list with the numbers from 0 up to (excluding) the given number, with an optional step
listCreates a list from its positional arguments
errorThrows an error with the given message
printlnPrints its arguments, primarily useful for debugging
toStrConverts any value, including null, to a string
isNumber, isString, isBoolean, isObject, isFunctionType checks
!Negates a boolean
-Negates a number
noeditTakes a locally defined function, executes it immediately and marks it as not editable from the graphical editor

The condition of while is a function, so that it can be evaluated again for each iteration:

HyLiMo
i = 0
while { i < 10 } {
    println(i)
    i = i + 1
}

Global constants

null, true and false.

Putting it together

Nothing in the diagram DSL is magic - it is this language plus a set of functions. The example below defines a function which creates a class with a standard set of members, and uses a loop to place several of them; the same techniques you would use in any other language.

Execution limit

A diagram is re-interpreted on every keystroke, so an endless loop would hang the editor. The interpreter therefore aborts after a fixed number of execution steps and reports an error.