What is it?
Skew is a programming language for building cross-platform software. It compiles to straightforward, readable source code in other languages and is designed to be easy to integrate into a mixed-language code base. The main focus of the project has been to develop solid, production-quality code for the web. Skew looks like this:
# Execution starts at the entry point @entry def fizzBuzz { for i in 1..101 { var text = mod(i, 3, "Fizz") + mod(i, 5, "Buzz") document.write((text == "" ? i.toString : text) + "\n") } } # The compiler inlines this function in release mode def mod(i int, n int, text string) string { return i % n == 0 ? text : "" } # Imported declarations are just for type checking @import namespace document { def write(text string) }
The language and compiler are in development and are still somewhat subject to change. The compiler is bootstrapped, which means it's written in Skew and compiles itself. It currently contains production-quality JavaScript generation and working C# generation. C++ generation is next and is already in progress.
Why use it?
The intent is to use this language for the platform-independent stuff in an application and to use the language native to the target platform for the platform-specific stuff. When done properly, the vast majority of the code is completely platform-independent and new platforms can be targeted easily with a small platform-specific shim.
Pros:- Advanced optimizations: The optimizations in the compiler combined with certain language features (integers, explicit exports, wrapped types, etc.) makes writing compact, efficient JavaScript code easy and pleasant.
- Fast compile times: Code compiles at the speed of a browser refresh. Web development still feels like web development despite using an optimizing compiler with static typing. This is in contrast to many other comparable compile-to-JavaScript languages.
- Natural debugging experience: Debugging is done in a single language using the platform-native debugger. No need to try to debug a multi-language app with a debugger that only understands one language.
- Easy integration: Generated code is very readable and closely corresponds with the original. Language features allow for the easy import and export of code to and from the target language.
- Fast iteration time: In addition to a fast compiler and a good debugging experience, garbage collection is used instead of manual memory management. This eliminates a whole class of time-consuming bugs that get in the way of the important stuff.
- Native code emission: For native targets, application logic is compiled directly to native code and is not interpreted in a virtual machine. Native apps don't have to pay for JIT warmup time and native app performance is not at the whim of heuristics. The generated code can be compiled using industry-standard compilers that leverage decades of optimization work.
- Lack of IDE support: IDE support is planned but is a significant undertaking and will not materialize for a while. Developers who normally lean heavily on IDEs will be less efficient than usual.
- Immaturity: This is a new programming language and hasn't stood the test of time. There will likely be many rough edges both in the language design and in the tools. Many planned features are not yet implemented.
- Lack of community: New programming languages don't have the wealth of searchable Q&A data that established programming languages have. Solutions to random issues are likely not available online.
- No cross-platform multithreading: Multithreading is not a language feature and needs to be done in the target language. This limits multithreading opportunities to cleanly separable tasks like image decoding.
- Lack of low-level features: Features such as memory layout, move semantics, destructors, and vector instructions are intentionally omitted. These features don't map well to all language targets and their emulation is expensive. Use of these features is limited to imported library routines implemented in the target language.
Getting Started
Installation
First, install node if you don't have it already. The release version of the compiler is cross-compiled to JavaScript and node is a JavaScript runtime. Installing node also installs a package manager called npm that can be used to install the compiler:
sudo npm install -g skew
The compiler command is called "skewc". Run "skewc --help" to see a list of all available command-line flags. Example usage:
skewc src/*.sk --output-file=compiled.js --release
Examples
Here are some simple examples to start from. Each example demonstrates how to do input and output using imported code from the target language:
-
Create a file called "sparks.sk" that looks like this:
Invoke the compiler to generate JavaScript code. Any compilation errors will show up here:
skewc sparks.sk --output-file=sparks.js
Create another file called "index.html" to serve the compiled code:
<body></body> <script src="sparks.js"></script>
That's it! Open "index.html" in a browser to see the app. To build a production-ready version of your app, just add the "--release" flag to the end of the compiler invocation:
skewc sparks.sk --output-file=sparks.js --release
-
Create a file called "calculator.sk" that looks like this:
Invoke the compiler to generate JavaScript code. Any compilation errors will show up here:
skewc calculator.sk --output-file=calculator.js
That's it! Run the generated code to see the app:
node calculator.js
-
Create a file called "calculator.sk" that looks like this:
Invoke the compiler to generate C# code. Any compilation errors will show up here:
skewc calculator.sk --output-file=calculator.cs
Compile the C# code to generate a .NET executable. The example here uses Mono, an open-source implementation of Microsoft's .NET Framework.
mcs calculator.cs -out:calculator.exe
That's it! Run the generated executable to see the app:
mono calculator.exe
All of these examples use the "dynamic" type for simplicity, but you'll likely want type imports for real work. Skew will eventually have a package manager and type imports will live there. I've put some HTML5 type imports on GitHub for convenience in the meantime.
An important thing to keep in mind when developing with Skew is that the compiler does dead code elimination. This means the compiler won't generate any output if nothing is marked for export. You can either indicate a "main" function with the "@entry" annotation as is done in the examples above, or you can use the "@export" annotation to ensure certain functions are exported.
To get a better feel for the language, it's probably helpful to take a look at the different examples of Skew code in the live editor in addition to looking at the documentation below.
IDE support
-
Visual Studio Code
Install the skew package for syntax highlighting, inline errors, type tooltips on hover, and other nice IDE features. Visual Studio Code is a cross platform Chromium-based text editor that works on Windows, OS X, and Linux and has nothing to do with the original Visual Studio product. Installing extensions in Visual Studio Code is a little strange. Use Ctrl+P to bring up the in-app command line, then type "ext install skew-vscode" and press enter when it loads. -
Sublime Text
Follow these instructions to install syntax highlighting. Right-click and using the "Goto Definition" command sort of works (it looks for all declarations with that name).
Language Reference
This reference is collapsed by default to make it easy to quickly get to different entries. Either click an entry below to expand it or expand all entries in this section to read them all at once.
-
Skew has a handful of built-in types with special literals. The code below is not valid (Skew only allows declarations at the top level) but it demonstrates the various kinds of literals and their types:
# Type "bool" false true # Type "int" 0b101 # Binary 0o123 # Octal 123 # Decimal 0xABC # Hex 'a' # Character # Type "double" 0.5 1e-6 1e100 # Type "string" "abc" "multi-line strings" # Type "List<int>" [1, 2, 3] # Type "StringMap<int>" {"a": 1, "b": 2} # Type "IntMap<int>" {1: 2, 3: 4}
For more information, see the standard library documentation for each type.
-
Variables are declared with the "var" keyword. Unlike C-like languages, the type comes after the variable name. This makes blocks of variables much more readable because the variable name is more important than the variable type. If the type is omitted, the type is inferred from the assigned value.
var explicitlyTyped int = 0 var implicitlyTyped = 0
Read-only variables are declared with the "const" keyword instead of the "var" keyword. This is similar to the "final" keyword in Java. It is not similar to the "const" keyword from C++; the object referenced by the variable can still be mutated. The compiler just ensures the variable cannot be reassigned.
const readOnly = 0 @export def test { readOnly = 1 # error: Cannot store to constant symbol "readOnly" }
Constant values can be overridden at compile time using the "--define" flag. For example, the flag "--define:readOnly=2" would cause "readOnly" to be initialized to 2 instead of 0 in the code above. This is useful for supporting multiple build configurations through conditional compilation.
-
Functions are declared with the "def" keyword. Parentheses are not used for functions that don't take any arguments, both when declaring the function and when calling it. This cuts out on a lot of unnecessary clutter that is present in other languages while still remaining unambiguous (functions must always be called and cannot be used by value). Absence of a return type is indicated by just not specifying a return type instead of requiring a special "void" type as other languages like C do.
def withArguments(x int, y int, z int) int { return 0 } def withoutArguments int { return 0 } @export def test { withArguments(1, 2, 3) withoutArguments }
Function overloading is supported. The compiler does overload resolution at compile-time and makes sure each overload has a unique name before code generation begins.
# This will be renamed "overloaded1" def overloaded(x int) { } # This will be renamed "overloaded2" def overloaded(x string) { } @export def test { overloaded(123) overloaded("text") }
Abstract and imported functions cannot be implemented. This is accomplished by omitting the body in the function declaration:
@import def print(text string) @export def test { print("works") }
Setter-style functions are declared by adding a "=" to the end of the function name. Because Skew supports function overloading, a getter and a setter for the same name can coexist:
def value double { return 0 } def value=(x double) { } @export def test { value = (value + 0.5) / 2 }
Multiple declarations can exist for the same function, although there can only be one implementation. This is sometimes useful when organizing code for clarity and is especially useful when combined with conditional compilation. It's sort of comparable to forward declarations in C/C++ although they are order-independent and don't need to come first.
-
Unlike in C, all conditional statements omit the parentheses surrounding the condition and require braces:
@export def test(a bool, b bool) { if a {} else if b {} else {} }
If statements cannot be used as expressions. Use C-style conditional expressions instead:
@export def test(a bool) int { return a ? 1 : 2 }
-
Skew has several different types of loops. Each loop below loops from 0 to 4 inclusive:
@export def test { # While loop var a = 0 while a < 5 { a++ } # Range for loop (lower inclusive, upper exclusive) for b in 0..5 { } # C-style for loop for c = 0; c < 5; c++ { } # For-in loop (currently only for List<T>) for d in [0, 1, 2, 3, 4] { } }
Loop control flow is done using the same "break" and "continue" keywords that C-like languages use.
-
Switch statements look a little different than they do in C. The fallthrough feature of C-style switch statements was deliberately removed since it's rarely used and is a recipe for bugs.
@export def test(x int) bool { switch x { case 1, 2, 3 { return true } case 4, 5 { return false } default { return x > 0 } } }
Unlike in C, the "break" keyword is only used with loops. Using a break inside a "case" block will break from the enclosing loop instead:
@export def validateEscapes(text string) bool { var slash = false for i in 0..text.count { switch text[i] { case '\\' { slash = !slash } case 'n', 't' { slash = false } default { if slash { break } } # Here "break" stops the loop } } return !slash }
-
Return statements use the standard "return" keyword. The type of the returned value is specified after the function name. Omitting the return type means a return type of "void".
def returnsSomething int { return 0 } def returnsNothing { } @export def test { returnsSomething returnsNothing }
One unusual thing about return statements is the handling of return statements where the value is on the next line. This case is trivial in semicolon-terminated languages like C because the value comes before the semicolon. In languages like JavaScript, automatic semicolon insertion gets in the way and causes the code to behave incorrectly (return undefined instead of the value). Skew doesn't suffer from this issue despite not having semicolons or automatic semicolon insertion. In Skew, an expression following a return statement is considered to be the returned value.
@export def add(a int, b int) int { return a + b } @export def subtract(a int, b int) { return a - b # error: Cannot return a value inside a function without a return type }
-
A lambda expression is an anonymous function that can be stored in a variable. They use a different syntax than regular functions. Regular functions declared using "def" are not lambda expressions and cannot be casted to a lambda type since they cannot be stored in a variable in some language targets (Java, for example). The "=>" symbol is used to separate the argument list and the function body.
@export def test int { var add = (x int, y int) int => { return x + y } return add(1, 2) }
Lambda expressions are lexical closures, meaning they have mutable access to the symbols they use. Subsequent calls to the same lambda expression will remember the old values.
@export def test int { var x = 0 var add = (y int) => { x += y } add(1) add(2) return x # The value of "x" is 3 here }
Lambda types are specified using the "fn" keyword. The syntax is similar to the syntax for defining a function except argument names are not included (only argument types) and parentheses are required.
var add fn(int, int) int = null var callback fn() = null @export def test { add = (x int, y int) int => { return x + y } callback = () => { } }
Since packaging up and manipulating little bits of code is so convenient, there are several shortcuts for creating more concise lambda expressions. The argument and/or return types can be omitted when they can be inferred from context.
@export def test { # These are all equivalent var zero1 = => 0 var zero2 = () => 0 var zero3 = () int => 0 var zero4 = () int => { return 0 } # These are both equivalent var add1 = (x int, y int) => x + y var add2 fn(int, int) int = (x, y) => x + y }
-
A class provides an object template that can be used to create instances of that class. A constructor is a function inside a class called "new" with no return type. The constructor's job is to set up the new class instance with initial values for all instance variables. Use "self" to refer to the class instance.
@export class Node { var weight int var name string var children List<Node> def new(name string, children List<Node>) { weight = 0 self.name = name self.children = children } }
When not explicitly declared, constructors are automatically generated with one argument for each instance variable without a default value in declaration order. This greatly simplifies defining objects in many situations. For example, the above code can be simplified to the code below since the constructor can be generated automatically. Unlike Java, constructors are just members of the type they construct and don't require a special operator to invoke:
class Node { var weight = 0 var name string var children List<Node> } @export def test Node { return Node.new("x", [ Node.new("y", []), Node.new("z", []), ]) }
-
Skew's object model is similar to Java. A class can inherit from at most one base class and can implement any number of interfaces. The symbols ":" and "::" are used instead of the "extends" and "implements" keywords.
interface Interface { def interfaceFunction int } class BaseClass { var value = 1 def baseFunction int { return value } } class DerivedClass : BaseClass :: Interface { def interfaceFunction int { return baseFunction + 2 } } @export def test int { var derived = DerivedClass.new var base BaseClass = derived var face Interface = derived return base.baseFunction + face.interfaceFunction }
Overriding a function can be error-prone in the presence of function overloading. To fix this, overriding a function must be done using the "over" keyword instead of the "def" keyword. The compiler checks that all functions declared using "over" are actually overriding something. That way changing the type signature of the overridden function is a compile error unless the type signature of the overriding function is also updated. The overridden function can be called from within the overriding function using the "super" keyword.
class BaseClass { var factor double def new(factor double) { self.factor = factor } def scale(x double) double { return x * factor } } class DerivedClass : BaseClass { def new { super(2) } over scale(x double) double { return super(x) * 3 } } @export def test double { var derived = DerivedClass.new var base = derived as BaseClass return base.scale(1) }
Unlike Java, all type declarations are "open", which means members from duplicate type declarations for the same type are all merged together at compile time into a single type. This allows for large type declarations to be better organized. In the example below, the "ChunkedBuffer" type can be made to implement the "Encoder" interface using a separate declaration.
class Buffer { var data = "" def append(data string) { self.data += data } } class ChunkedBuffer : Buffer { over append(data string) { super("[" + data + "]") } } interface Encoder { def encodeInt(value int) def encodeString(value string) } class ChunkedBuffer :: Encoder { def encodeInt(value int) { append(value.toString) } def encodeString(value string) { append(value) } } class User { var id int var name string def encode(encoder Encoder) { encoder.encodeInt(id) encoder.encodeString(name) } } @export def test { User.new(0, "name").encode(ChunkedBuffer.new) }
Another use of open type declarations is for safe compile-time monkey patching:
class double { def radToDeg double { return self * 180 / Math.PI } def degToRad double { return self * Math.PI / 180 } } @export def test double { return Math.PI.radToDeg }
-
Generics are implemented using type erasure to ensure a compact and readable implementation in the generated code. The current implementation is pretty simple. There isn't any type inference yet, and also no advanced features like covariant or contravariant conversions.
class Pair<A, B> { const first A const second B } @export def pack<A, B>(first A, second B) Pair<A, B> { return Pair<A, B>.new(first, second) }
-
An enum is a compile-time integer constant. Enums automatically convert to ints but ints don't automatically convert to enums. When referencing an enum value, the enum type can be omitted when it can be inferred from context. This leaves just a leading "." character.
@export class Entry { # Enums are assigned from the sequence 0, 1, 2, 3, ... enum Kind { FILE DIRECTORY SYMLINK } const kind Kind const children List<Entry> def visitFiles(visitor fn(Entry)) { switch kind { # This is short for "case Entry.Kind.FILE" case .FILE { visitor(self) } # This is short for "case Entry.Kind.DIRECTORY" case .DIRECTORY { for child in children { child.visitFiles(visitor) } } # This is short for "case Entry.Kind.SYMLINK" case .SYMLINK { } } } }
For convenience, each enum type automatically generates a "toString" function if one isn't present. Enums can have instance functions just like any other object type. Instance functions are automatically rewritten as global functions during compilation that take the enum as an extra first argument.
enum Tag { DIV def toHTML string { return "<" + toString.toLowerCase + ">" } } @export def test string { return Tag.DIV.toHTML # This returns "<div>" }
There is also built-in support for integer flags. Use a "flags" declaration instead of an "enum" declaration. Unlike normal enums, these special enums get "~", "&", "|", "^", and "in" operators. The literal "0" also stands for the empty set.
# Flags are assigned from the sequence 1, 2, 4, 8, ... flags Modifiers { COMMAND CONTROL OPTION SHIFT } @export def test(modifiers Modifiers) { modifiers |= .COMMAND modifiers &= ~.SHIFT if .OPTION in modifiers { modifiers = 0 # The value "0" is the empty set } if .CONTROL in modifiers { modifiers = ~0 # The value "~0" sets all flags } }
-
Casting is done using the "as" operator. These are useful for converting between primitive types and also for downcasting from a base class to a derived class. Downcasts are unchecked and have no performance impact in dynamic language targets, where they disappear entirely.
class Base {} class Derived : Base {} @export def test { var truncated = 3.5 as int var base Base = Derived.new var downcasted = base as Derived }
-
String interpolation is the term for a special escape sequence that embeds an arbitrary expression inside a string. This is a useful shortcut because it will automatically call the "toString" function on the expression.
@export def test(a int, b int) { # These two statements are equivalent var x = a.toString + " + " + b.toString + " = " + (a + b).toString var y = "\(a) + \(b) = \(a + b)" }
-
These function similar to other languages.
@export def test { try { switch (Math.random * 3) as int { case 0 { throw A.new } case 1 { throw B.new } case 2 { throw C.new } } } catch temp A {} # This will only catch "A" catch temp B {} # This will only catch "B" finally {} # "C" is never caught and will propagate upwards } class A {} class B {} class C {}
-
Names that start with "_" have protected access. They are only available to that class and its derived classes.
class Test { var _value = 0 def test { _value++ # This works fine } } @export def main(test Test) { test._value++ # error: Cannot access protected symbol "_value" here }
Protected access works for any named scope including namespaces and interfaces.
-
All variables and functions inside a class declaration are attached to instances of that class. To scope global variables and functions inside the class name, put them inside a namespace with the same name as the class. This does what the "static" keyword does in many other languages.
class Demo { var instanceVariable = 0 def instanceFunction {} } namespace Demo { var globalVariable = 0 def globalFunction {} } @export def test { Demo.new.instanceVariable Demo.new.instanceFunction Demo.globalVariable Demo.globalFunction }
This works for all types with instances including enums and interfaces.
-
Skew has quite a few operators. Many of them are described in detail after this. The ones that may be unfamiliar and that aren't described in detail are the "**" exponent operator, the "%%" modulus operator, and the ">>>" unsigned right shift operator. Here's the complete list for quick reference:
# Unary, overridable + - ~ ++ -- # Binary, overridable <=> [] in + - * / % %% ** & | ^ << >> >>> += -= *= /= %= **= &= |= ^= <<= >>= >>>= # Ternary, overridable []= # Binary, not overridable = && || == != < > <= >= ?. ?? ?=
An operator is overloaded by declaring an instance function on the target type with that operator name. For binary operators, the compiler checks for operator overloads on the left operand. Operator overloading can make code more readable in many cases, but can also dramatically reduce readability when used incorrectly. Use with good judgement.
enum Axis { X Y } class Vector { var x double var y double def -(p Vector) Vector { return new(x - p.x, y - p.y) } def [](a Axis) double { return a == .X ? x : y } def []=(a Axis, v double) { if a == .X { x = v } else { y = v } } } @export class Player { var position Vector def deltaTo(other Player) Vector { return other.position - position } def moveAlongAxis(axis Axis, delta double) { position[axis] = position[axis] + delta } }
The "in" operator is a special case. It looks for operator overloads on the right operand instead of the left operand since that allows it to work with generic types:
class Container<T> { def in(value T) bool def add(value T) } @export def addIfMissing<T>(value T, container Container<T>) { if !(value in container) { container.add(value) } }
Another interesting operator is the "<=>" comparison operator. It returns an integer that is less than zero if the left operand is less than the right, greater than zero if the left operand is greater than the right, and zero if the left operand is equal to the right. It is used by the compiler to automatically implement the "<", ">", "<=", and ">=" operators, which cannot be implemented manually.
class Point { var x int var y int def <=>(p Point) int { var delta = x <=> p.x return delta != 0 ? delta : y <=> p.y } } @export def test(list List<Point>) { var zero = Point.new(0, 0) list.sort((a, b) => a <=> b) list.removeIf(p => p < zero) }
Mutating operators can be synthesized from non-mutating operators. For example, a type that defines the "+" operator also gets the "+=" and "++" operators automatically:
@rename("goog.math.Long") @import class Long { @rename("add") def +(x Long) Long def +(x int) Long { return self + fromInt(x) } } namespace Long { def fromInt(value int) Long } @export def test { var a = Long.fromInt(123) a += Long.fromInt(234) a += 5 a++ }
The "?.", "??", and "?=" operators make handling nullable values easier and safer. They are inspired by the same operators from C#. The expression "a?.b" is short for "a != null ? a.b : null", the expression "a ?? b" is short for "a != null ? a : b", and the expression "a ?= b" is short for "a != null ? a : a = b". The compiler ensures sub-expressions are only evaluated once.
class Log { def info(text string) { console.log(text) } } def dump(log Log, text string) { log?.info(text ?? "(null)") } @export def test(log Log) { # Create a log if one wasn't passed in log ?= Log.new # Passing null text is fine (prints "(null)" instead) dump(log, "blah") dump(log, null) # Passing a null log is fine too (nothing is printed) dump(null, "blah") dump(null, null) } @import var console dynamic
The "==" and "!=" operators cannot be overloaded because they test for reference equality when using generics and this would lead to subtle bugs since generics are implemented with type erasure. The "&&" and "||" operators cannot be overloaded because of their special short-circuit evaluation behavior. The "=" operator cannot be overridden since that's too confusing in a language that lacks value types. If the "=" were overridable, objects would then be copied on assignment but would be passed by reference as arguments to function calls.
-
The syntax for list and map literals isn't just for the native list and map types; it's also available to user-defined types. The simplest way of doing this is with special constructors called "[new]" and "{new}" that take lists as arguments. The compiler puts all expressions inside the list or map literal in a list and passes it to the constructor.
class DemoList<T> { def [new](values List<T>) {} } class DemoMap<K, V> { def {new}(keys List<K>, values List<V>) {} } @export def test { var list DemoList<int> = [1, 2, 3] var map DemoMap<int, int> = {1: 2, 3: 4} }
The more flexible way of doing this is with the special "[...]" and "{...}" instance functions. The compiler constructs a new object of that type and builds a chain of calls to those special instance functions with one call for each element in the list or map literal. This allows the type to support different types of values in the same literal.
class DemoList { def [...](value int) DemoList { return self } def [...](value string) DemoList { return self } } class DemoMap { def {...}(key int, value int) DemoMap { return self } def {...}(key string, value string) DemoMap { return self } } @export def test { var list DemoList = [1, "2"] var map DemoMap = {1: 2, "3": "4"} }
-
XML-style object initialization syntax is supported as a convenient way to initialize trees of objects. Each tag constructs an instance of the type corresponding to the tag name. All attributes in the XML tag become assignments to variables on that instance. Child elements are appended using special instance functions called "<>...</>".
@entry def test { var multiply = <Reduce start=1 reduce=((a, b) => a * b.value)> for i in [2, 3, 4] { <Number value=i/> } </Reduce> document.write(multiply.value) } class Node { var children List<Node> = [] # The compiler uses this function to append children def <>...</>(child Node) { children.append(child) } def value double } class Reduce : Node { var start = 0.0 var reduce fn(double, Node) double = null over value double { var result = start for child in children { result = reduce(result, child) } return result } } class Number : Node { var _value = 0.0 over value double { return _value } # Setter functions also work with XML literals def value=(value double) { _value = value } } @import const document dynamic
-
Wrapped types are defined using the "type" keyword. They are like type aliases but they must be explicitly casted, which can be used to improve type safety.
type CSV = string @export def escapeForCSV(text string) CSV { if "\"" in text || "," in text || "\n" in text { return ("\"" + text.replaceAll("\"", "\"\"") + "\"") as CSV } return text as CSV }
Wrapped types can also be used to add encapsulation without the overhead of additional allocation. Instance functions added to wrapped types are automatically rewritten as global functions during compilation that take the instance as an extra first argument.
Another use of wrapped types is to provide a nice object-oriented API on top of an index into an array of data. This is sort of analogous to pointers in C, although they don't need to all be in the same address space like emscripten and aren't subject to fragmentation issues. Pointer-style wrapped types can generate very tight code in release mode.
# This represents a "pointer" to a node in a tree type Node<T> : int { def value(tree Tree<T>) T { return (tree as List<T>)[self as int] } def hasChildren(tree Tree<T>) bool { return rightChild as int < tree.nodeCount } def parent Node<T> { return ((self as int - 1) >> 1) as Node<T> } def leftChild Node<T> { return ((self as int) * 2 + 1) as Node<T> } def rightChild Node<T> { return ((self as int) * 2 + 2) as Node<T> } } # This is a binary tree stored as a flat list type Tree<T> : List<T> { def root Node<T> { return 0 as Node<T> } def nodeCount int { return (self as List<T>).count } def toString(cb fn(T) string) string { return _toString(root, cb, "\n ") } def _toString(node Node<T>, cb fn(T) string, indent string) string { if !node.hasChildren(self) { return "- " + cb(node.value(self)) } return "- " + cb(node.value(self)) + indent + _toString(node.leftChild, cb, indent + " ") + indent + _toString(node.rightChild, cb, indent + " ") } } namespace Tree { def new(values List<T>) Tree<T> { return values as Tree<T> } } @entry def test { var tree = Tree<int>.new([4, 2, 6, 1, 3, 5, 7]) document.write(tree.toString(x => x.toString)) } @import const document dynamic
Wrapped types can also be used to create safe immutable wrappers for other APIs. If they use function inlining correctly, these wrappers won't have any runtime performance overhead. For example, the following code implements a simple read-only list type with the same efficiency of a built-in mutable list in release mode:
# An immutable wrapper for a List<T> that is safe to share. # Call "clone" to get back a mutable list. type FrozenList<T> : List<T> { def [](index int) T { return (self as List<T>)[index] } def count int { return (self as List<T>).count } def isEmpty bool { return (self as List<T>).isEmpty } def thaw List<T> { return (self as List<T>).clone } def in(value T) bool { return value in (self as List<T>) } } class List { def freeze FrozenList<T> { return clone as FrozenList<T> } } @export def test { var list = [1, 2, 3] list.append(4) var frozen = list.freeze frozen.append(5) # error: "append" is not declared on type "FrozenList<int>" var thawed = frozen.thaw thawed.append(6) }
-
Top-level if statements allow for conditional code compilation. Like all declarations, top-level if statements are also order-independent and are evaluated from the outside in. Conditions must be compile-time constants but are fully type-checked, unlike the C preprocessor. Including preprocessing as part of the syntax tree ensures that there aren't syntax errors hiding in unused code branches. Constant values can be overridden at compile time using a command-line argument such as "--define:TRACE=true".
const TRACE = true if TRACE { var traceEnter = (label string) => { Log.info("[enter] " + label) Log.indent++ } var traceLeave = (label string) => { Log.indent-- Log.info("[leave] " + label) } } else { def traceEnter(label string) {} def traceLeave(label string) {} } @import namespace Log { var indent = 0 def info(text string) } @export def test { traceEnter("test") traceLeave("test") }
The compiler includes some predefined variables that are automatically set based on the compilation settings.
if TARGET == .CSHARP { class ParseError : System.Exception {} } else { class ParseError {} }
Another form of conditional compilation is the "@skip" annotation. Annotating something with "@skip" means that all call sites and their argument evaluations will be removed at compile time and will not be present in the output. This is cleaner and less error-prone than using top-level if statements for conditional compilation of functions because all uses are type-checked even when unused.
# Any annotation can be applied conditionally using the # post-if syntax. The condition must be a compile-time # constant just like for top-level if statements. @skip if RELEASE def debugLog(text string) { dynamic.console.log(text) } @export def sum(x int, y int) int { if x + y < 0 { debugLog("warning: \(x) + \(y) < 0") } return x + y }
-
There is a special type called "dynamic" that tells the compiler to ignore type errors and pass all dynamically-typed code through verbatim to the target language. It is mainly useful when interacting with external code, especially when targeting a dynamic language.
Simple example:
@import const console dynamic @export def test { console.log("works") }
The "dynamic" type poisons everything it touches. For example, "x + 1" has a dynamic type if "x" is dynamic because the compiler can't tell what type the expression will be ("x" could hold an int, a double, a string, or even something nonsensical like an object). This makes it easy to bypass the compiler's type system by casting the value to the "dynamic" type first.
Pros:
- Makes it easy to import large libraries without tons of type declarations.
- Provides a way to bypass compiler checks when there's a type system mismatch with the target language. For example, C# supports "in" and "out" modifiers on generic type parameters and Skew does not.
- Used by the compiler to silence further errors when an error occurs during compilation (expressions with compile errors always have type "dynamic").
Cons:
- Mistakes cause run-time errors instead of compile-time errors.
- Most compiler features don't work on dynamically typed code (implicit function calls, operator overloading, function overloading, type wrapping, inlining, constant folding, etc.). Statically-typed values can still be passed through a dynamically-typed environment but must be casted back to their static type before being used or they likely won't work correctly.
Values of dynamic type can also be used to emit constructor calls. This is done using the "new" property just like for statically-typed objects. Notice how function calls in the example below must be explicitly invoked via "()" unlike normal functions. This is because the compiler has no type information so it can't distinguish functions from variables.
@import const Object dynamic @export def test int { return Object.new().toString().length }
The keyword "dynamic" can also be used as an expression with a property name following it. This causes the following name to be emitted as an identifier with a dynamic type. It's mainly useful as an inline shortcut to avoid an imported declaration.
@export class Demo : dynamic.DemoBase { def run { dynamic.console.log("works") } }
Standard Library
This reference is collapsed by default to make it easy to quickly get to different entries. Either click an entry below to expand it or expand all entries in this section to read them all at once.
-
There are two bool values, "true" and "false".
class bool { def ! bool def toString string }
The "&&" and "||" operators also operate on bool values but cannot described using imported functions due to their special short-circuiting behavior.
-
An int is a signed 32-bit non-nullable integer. The supported bases are binary (0b101), octal (0o123), decimal (123), and hex (0xABC).
class int { def + int def ++ int def - int def -- int def ~ int def +(x int) int def -(x int) int def *(x int) int def /(x int) int def %(x int) int # Remainder, not modulus (different for negative values) def %%(x int) int # Modulus operator (wraps from 0 to x) def **(x int) int # Exponential operator def <=>(x int) int # Makes "<", ">", "<=", and ">=" work def <<(x int) int def >>(x int) int # Signed shift operator def >>>(x int) int # Unsigned shift operator def &(x int) int def |(x int) int def ^(x int) int def +=(x int) int def -=(x int) int def /=(x int) int def *=(x int) int def %=(x int) int # Remainder, not modulus (different for negative values) def %%=(x int) int # Modulus operator (wraps from 0 to x) def **=(x int) int # Exponential operator def <<=(x int) int def >>=(x int) int # Signed shift operator def >>>=(x int) int # Unsigned shift operator def &=(x int) int def |=(x int) int def ^=(x int) int def toString string } namespace int { const MIN int const MAX int }
-
A double is a 64-bit non-nullable floating-point number. A number literal must have a "." or an exponential "e" in it to be a double, otherwise it's an int. The non-finite double constants are "Math.NAN" and "Math.INFINITY".
class double { def + double def ++ double def - double def -- double def +(x double) double def -(x double) double def *(x double) double def /(x double) double def %%(x double) double # Modulus operator (wraps from 0 to x) def **(x double) double # Exponential operator def <=>(x double) int # Makes "<", ">", "<=", and ">=" work def +=(x double) double def -=(x double) double def *=(x double) double def /=(x double) double def %%=(x double) double # Modulus operator (wraps from 0 to x) def **=(x double) double # Exponential operator def isFinite bool def isNaN bool def toString string }
-
A string is an immutable sequence of unicode code units. Multi-line strings are allowed without needing any special syntax. String literals must be double-quoted (single quotes are for C-style character literals, which turn into an int with the unicode code point for that character). Valid escape sequences are "\0", "\r", "\n", "\t", "\\", "\"", "\'", and hex-style "\xFF".
Strings are stored using the natural string encoding for the target language. For example, JavaScript uses UTF-16 and C++ uses UTF-8. All indices are in terms of code units since code unit access can be done in constant time. To work in platform-independent code points instead, use the code point APIs and the "Unicode.StringIterator" class.
class string { def +(x string) string def +=(x string) string def <=>(x string) int # Makes "<", ">", "<=", and ">=" work def count int # Counts code units def in(x string) bool # Makes the "x in y" syntax work def indexOf(x string) int # Index is in code units def lastIndexOf(x string) int # Index is in code units def startsWith(x string) bool def endsWith(x string) bool def [](x int) int # Returns a single code unit def get(x int) string # Returns a single code unit def slice(start int) string # Slices code units def slice(start int, end int) string # Slices code units def codePoints List<int> def codeUnits List<int> def split(x string) List<string> def join(x List<string>) string def repeat(x int) string def replaceAll(before string, after string) string def toLowerCase string def toUpperCase string } namespace string { def fromCodePoint(x int) string def fromCodePoints(x List<int>) string def fromCodeUnit(x int) string def fromCodeUnits(x List<int>) string }
-
In certain language targets, the "+=" operator on strings can be extremely inefficient. This API provides an efficient way to construct large strings.
class StringBuilder { def new def append(x string) def toString string }
-
A generic wrapper object for non-nullable types (bool, int, and double) to make them nullable. Also useful for nullable types to indicate the absence of a value when null is a valid value.
class Box<T> { var value T }
-
Provides a generic array that stores all items linearly in memory. Example usage:
@export def test { var list = [1, 2, 3] # Index into a list using bracket notation var mean = 0.0 for i in 0..list.count { mean += list[i] } mean /= list.count # Can also loop over items in a list directly var variance = 0.0 for item in list { variance += (item - mean) ** 2 } variance /= list.count }
Complete API:
class List<T> { def new def [...](x T) List<T> # Makes the [1, 2, 3] syntax work def [](x int) T def []=(x int, y T) T def count int def isEmpty bool def resize(count int, defaultValue T) def appendOne(x T) # Only append if the value isn't present def append(x T) def append(x List<T>) def prepend(x T) def prepend(x List<T>) def insert(x int, value T) def insert(x int, values List<T>) def removeAll(x T) def removeAt(x int) def removeDuplicates def removeFirst def removeIf(x fn(T) bool) def removeLast def removeOne(x T) def removeRange(start int, end int) def takeFirst T def takeLast T def takeRange(start int, end int) List<T> def first T def first=(x T) T def last T def last=(x T) T def in(x T) bool # Makes the "x in y" syntax work def indexOf(x T) int def lastIndexOf(x T) int def all(x fn(T) bool) bool def any(x fn(T) bool) bool def clone List<T> def each(x fn(T)) def equals(x List<T>) bool def filter(x fn(T) bool) List<T> def map<R>(x fn(T) R) List<R> def reverse def shuffle def slice(start int) List<T> def slice(start int, end int) List<T> def sort(x fn(T, T) int) def swap(x int, y int) }
-
Provides a generic map where reading and writing is done in amortized constant time. Example usage:
@export def test(key int) { var map = {1: 2, 3: 4} if !(key in map) { map[key] = 0 } map[key]++ }
Complete API:
class IntMap<T> { def new def {...}(key int, value T) IntMap<T> # Makes the {1: 2} syntax work def [](key int) T def []=(key int, value T) T def count int def isEmpty bool def keys List<int> def values List<T> def clone IntMap<T> def each(x fn(int, T)) def in(key int) bool # Makes the "x in y" syntax work def get(key int, defaultValue T) T def remove(key int) }
-
Provides a generic map where reading and writing is done in amortized constant time. Example usage:
@export def test(key string) { var map = {"a": 1, "b": 2} if !(key in map) { map[key] = 0 } map[key]++ }
Complete API:
class StringMap<T> { def new def {...}(key string, value T) StringMap<T> # Makes the {"a": "b"} syntax work def [](key string) T def []=(key string, value T) T def count int def isEmpty bool def keys List<string> def values List<T> def clone StringMap<T> def each(x fn(string, T)) def get(key string, defaultValue T) T def in(key string) bool # Makes the "x in y" syntax work def remove(key string) }
-
namespace Math { const E double const INFINITY double const NAN double const PI double const SQRT_2 double def abs(x double) double def abs(x int) int def acos(x double) double def asin(x double) double def atan(x double) double def atan2(x double, y double) double def sin(x double) double def cos(x double) double def tan(x double) double def floor(x double) double def ceil(x double) double def round(x double) double def exp(x double) double def log(x double) double def pow(x double, y double) double def sqrt(x double) double def random double def randomInRange(min double, max double) double def randomInRange(minInclusive int, maxExclusive int) int def max(x double, y double) double def min(x double, y double) double def max(x double, y double, z double) double def min(x double, y double, z double) double def max(x double, y double, z double, w double) double def min(x double, y double, z double, w double) double def max(x int, y int) int def min(x int, y int) int def max(x int, y int, z int) int def min(x int, y int, z int) int def max(x int, y int, z int, w int) int def min(x int, y int, z int, w int) int def clamp(x double, min double, max double) double def clamp(x int, min int, max int) int }
-
The string type stores unicode code units, not code points. See the documentation of "string" for more information.
namespace Unicode { enum Encoding { UTF8 UTF16 UTF32 } # This is set to the string encoding for the current language target const STRING_ENCODING Encoding # This provides an efficient way to iterate over a string and access unicode # code points. Code points must be traversed using iteration because they # are encoded using a variable number of code units in UTF-8 and UTF-16. class StringIterator { var value string var index int var stop int def reset(text string, start int) def countCodePointsUntil(stop int) int def nextCodePoint int } namespace StringIterator { # This is provided for convenience and can be used to avoid GC churn const INSTANCE StringIterator } }
Example usage:
@export def countCodePointsInString(text string) int { var iterator = Unicode.StringIterator.INSTANCE iterator.reset(text, 0) return iterator.countCodePointsUntil(text.count) } @export def codePointAtIndex(text string, index int) int { var iterator = Unicode.StringIterator.INSTANCE iterator.reset(text, 0) iterator.countCodePointsUntil(index) return iterator.nextCodePoint }
-
enum Target { NONE CPLUSPLUS CSHARP JAVASCRIPT } # This is set to the current language target const TARGET Target # This is true when the compiler is passed the "--release" flag const RELEASE bool
-
# This causes a runtime failure when passed "false". Calls to assert # will be completely removed in release mode, so arguments won't # be evaluated. This is accomplished with the "@skip" annotation. def assert(truth bool)
-
These annotations trigger special behaviors in the compiler for the symbols they annotate.
# Using "@alwaysinline" warns if inlining wasn't possible def @alwaysinline def @neverinline # These cause warnings when the annotated symbol is used def @deprecated def @deprecated(message string) # There can only be one active entry point during compilation. It must take # no arguments or take a List<string> and return either nothing or an int. def @entry # Imported code is assumed to exist and will not appear in the compiled result def @import # Exported code isn't dead stripped, minified, or otherwise altered def @export # This influences overload resolution for ambiguous matches def @prefer # Change the name of the annotated symbol during code generation def @rename(name string) # This causes calls to the function to be completely removed after type # checking. This means the arguments will not be evaluated at the call site. def @skip # This is meant to be used for other annotations. When those annotations are # on a function, "@spreads" causes those annotations to be propagated to # other functions when that function is inlined inside them. def @spreads # This causes a C# "using" statement to be emitted when the symbol is used def @using(name string) # This causes a C++ "#include" pragma to be emitted when the symbol is used def @include(name string)
Compiler Optimizations
The compiler currently includes quite a few different optimizations aimed at reducing size code when compiling to JavaScript to speed up network transfer time. Most of these passes can be enabled individually with compiler flags but they are all enabled with the "--release" flag.
This reference is collapsed by default to make it easy to quickly get to different entries. Either click an entry below to expand it or expand all entries in this section to read them all at once.
-
The compiler uses a complex lattice of rules to compact the generated JavaScript code in release mode similar to the advanced compilation mode in Google Closure Compiler. However, the compiler can often do better than Google's compiler due to certain language features (explicit integer types, for example). This reduces application startup time by transferring less data over the network. The Skew compiler is also a whole lot faster than Google's compiler so advanced optimizations can be enabled by default.
enum Kind { RADIANS DEGREES SECONDS METERS } @export def format(kind Kind, value double) string { if kind == .DEGREES || kind == .RADIANS { if kind == .RADIANS { value *= 180 / Math.PI } return "\(value)deg" } if kind == .SECONDS { return "\(value)s" } assert(kind == .METERS) return "\(value)m" }
The code above reduces down to this JavaScript code in release mode:
(function(){format=function(a,b){return a==1||!a?(a||(b*=57.29577951308232),b+'deg'):b+(a^2?'m':'s')}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--js-mangle" flag.
-
The compiler includes a global symbol motion and renaming pass in release mode to reduce symbol name length similar to the advanced compilation mode in Google Closure Compiler. Namespace nesting is removed and symbols are renamed to the shortest possible names using frequency analysis. Unrelated properties and local variables can reuse the same names, which makes those names even more common and easier for gzip to compress. This reduces application startup time by transferring less data over the network. It also serves as a form of obfuscation if you care about that sort of thing. Renaming is not done for symbols with an "@import" or "@export" annotation and for names accessed off of the special "dynamic" type.
namespace Demo { class Vector { var x double var y double } class Rect { var lower = Vector.new(Math.INFINITY, Math.INFINITY) var upper = Vector.new(-Math.INFINITY, -Math.INFINITY) def area double { return (upper.x - lower.x) * (upper.y - lower.y) } def include(x double, y double) { lower.x = Math.min(lower.x, x) lower.y = Math.min(lower.y, y) upper.x = Math.max(upper.x, x) upper.y = Math.max(upper.y, y) } } } @export def test double { var rect = Demo.Rect.new for i in 0..100 { rect.include(Math.random, Math.random) } return rect.area }
The code above becomes this JavaScript code in release mode. Notice how the properties for both "Rect" and "Vector" can reuse the same names because they are two unrelated types:
(function(){test=function(){for(var a=new i,b=0;b<100;b=b+1|0)f(a,Math.random(),Math.random());return e(a)};function e(a){return (a.b.a-a.a.a)*(a.b.b-a.a.b)}function f(d,a,b){d.a.a=Math.min(d.a.a,a),d.a.b=Math.min(d.a.b,b),d.b.a=Math.max(d.b.a,a),d.b.b=Math.max(d.b.b,b)}function h(a,b){this.a=a,this.b=b}function i(){this.a=new h(Infinity,Infinity),this.b=new h(-Infinity,-Infinity)}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--js-mangle" flag.
-
The compiler currently inlines simple functions where the body consists of a single expression statement or return statement and where each argument is used once.
class WrappedValue<T> { var _value T def value T { return _value } def value=(value T) { _value = value } } @export def test { var wrapped = WrappedValue<int>.new(0) wrapped.value = wrapped.value + 100 }
The code above becomes this JavaScript code in release mode. Notice how the getter and setter functions for "value" were both inlined into a simple assignment:
(function(){test=function(){var a=new c(0);a.a=a.a+100|0};function c(a){this.a=a}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--inline-functions" flag.
-
Constant folding does partial evaluation of the code at compile-time. This often opens up opportunities for further optimizations such as dead code elimination inside the "else" branch of an "if true" statement. Constant folding works well in combination with with function inlining.
The code above becomes this JavaScript code in release mode. Notice how all getters have been inlined, the call to the constructor with constant arguments became a single integer literal, and the call to the constructor inside "opaque" has been partially evaluated by performing "r << 16 | g << 8 | b" at compile time:
(function(){isOrange=function(a){return b(a)==-33024};function b(a){return a|255<<24}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--fold-constants" flag.
-
JavaScript only has one set of arithmetic operators and they operate on floating-point values. CPUs are much faster when working with integers so JIT compilers try really hard to figure out when the code is using numbers that behave like 32-bit integers so they can generate machine code for 32-bit integers instead of floating-point numbers and get a big speedup.
However, this guesswork isn't a perfect solution. The code is now at the whim of the JIT heuristic engine, which may or may not be able to guess correctly. These guesses are likely not even consistent between successive runs of the same code due to different optimization decisions being made based on changing timing measurements and/or execution flow. Missing these optimizations can sometimes cause drastic performance issues, especially inside tight inner loops of math-heavy code. Even if the JIT guesses that a particular arithmetic operator can be an integer correctly, it often doesn't have enough range information to be able to eliminate bounds checks, so the generated assembly is littered with overflow checks and deoptimization bailouts.
Skew has actual integer operations, even when compiling to JavaScript. It ensures every integer arithmetic operation is wrapped in a bitwise operation. All modern JITs omit bounds checks when they can prove the checks are unnecessary, which is always the case for integer addition, subtraction, division, and remainder operations when wrapped in a bitwise operation. Integer multiplication operations actually can't be done with the floating-point "*" operator because 64-bit doubles don't have enough bits to represent the result, but luckily modern JITs provide "Math.imul" to efficiently do 32-bit multiplication.
@export def test(seed int) int { var value = (seed * 1103515245 + 12345) & 0x7FFFFFFF # rand() from glibc value++ value /= 2 return value }
The code above becomes this JavaScript code. The polyfill for Math.imul is only for old JavaScript runtimes; new ones provide Math.imul.
(function() { var __imul = Math.imul ? Math.imul : function(a, b) { return (a * (b >>> 16) << 16) + a * (b & 65535) | 0; }; test = function(seed) { // rand() from glibc var value = __imul(seed, 1103515245) + 12345 & 2147483647; value = value + 1 | 0; value = value / 2 | 0; return value; }; })();
When given that JavaScript, V8 produces the following optimized assembly code (stack checks and tagging and stuff is omitted for clarity). Notice how the operands remain integers throughout the computation:
;;; <@30,#18> mul-i 65 69c96d4ec641 imul ecx,ecx,1103515245 ;;; <@32,#20> add-i 71 81c139300000 add ecx,0x3039 ;; debug: position 558 ;;; <@34,#23> bit-i 77 81e1ffffff7f and ecx,0x7fffffff ;; debug: position 566 ;;; <@36,#28> add-i 83 83c101 add ecx,0x1 ;; debug: position 667 ;;; <@38,#36> div-by-power-of-2-i 86 89c8 mov eax,ecx ;; debug: position 694 88 c1e81f shr eax,31 91 03c1 add eax,ecx 93 d1f8 sar eax,1
This transformation is always performed for correctness reasons, even in debug mode. Floating-point arithmetic operations are not equivalent to their integer counterparts and cannot be substituted without compromising the semantics of the code.
-
Virtual functions have overhead, mostly because they prevent inlining. Devirtualization detects functions that are unnecessarily virtual and rewrites them to global functions instead, which can then be optimized further. It is why function inlining works on instance functions.
class Demo { var x int var y int def add { x += y } } @export def test { Demo.new(1, 2).add }
The code above becomes this JavaScript code in release mode. Notice how the generated code doesn't use "prototype" anywhere even though the function "add" is an instance function:
(function(){test=function(){c(new e(1,2))};function c(a){a.a=a.a+a.b|0}function e(a,b){this.a=a,this.b=b}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--globalize-functions" flag.
-
Interfaces that only have one implementation can be safely replaced with that implementation. This opens up more optimization opportunities such as devirtualization and inlining.
# This interface has one implementation and can be removed interface IOne { def add } class Single :: IOne { var value = 0.0 def add { value++ } } # This interface has two implementations and cannot be removed interface ITwo { def add } class First :: ITwo { var value = 0.0 def add { value++ } } class Second :: ITwo { var value = 0.0 def add { value++ } } @export def test { var one IOne = Single.new one.add var two ITwo = First.new two.add two = Second.new two.add }
The code above becomes this JavaScript code in release mode. Notice how the call through the IOne interface, which only has one implementation, was completely devirtualized and inlined. This is only possible because the IOne interface was able to be removed.
(function(){test=function(){var a=new g;a.a++;var c=new h;c.b(),c=new i,c.b()};function g(){this.a=0}function h(){this.a=0}h.prototype.b=function(){this.a++};function i(){this.a=0}i.prototype.b=function(){this.a++}})();
When not enabled as part of the "--release" flag, this pass can be selectively enabled with the "--globalize-functions" flag.
-
This removes unused code at the function level. The compiler starts from functions with the "@export" and "@entry" annotations and finds all transitive dependencies. Any code that wasn't reached is treated as dead and removed from the output. This reduces application startup time by transferring less data over the network. This is also enabled in debug mode because lots of extra unused library code makes debugging harder.