constLightScript is const by default; the keyword is not necessary:
greeting = 'Hello, World!'const greeting = "Hello, World!";
This is also true when destructuring:
{ color, owner } = lightsaberconst { color, owner } = lightsaber;
Because LightScript is a (rough) superset of JavaScript,
the const keyword is also valid:
const greeting = 'Hello, World!'const greeting = "Hello, World!";
LightScript uses Facebook's Flow typechecker and type syntax, so you can optionally annotate types:
greeting: string = 'Hello, World!'const greeting: string = "Hello, World!";
As a rule of thumb, anywhere you can use Flow syntax in JavaScript, you can use the same syntax in LightScript.
Integration with the Flow typechecker has not been built yet,
so while you can annotate your types, they will not yet be statically checked.
This is blocking on Flow accepting an AST as input.
As a stopgap, the babel-preset-lightscript may include
tcomb, which provides runtime typechecks.
Note that, unlike in JavaScript, the : cannot be followed by a newline.
let and varlet and var are the same as in JavaScript.
let friendCount = 1let friendCount = 1;
However, to reassign a variable, you must use the keyword now:
let friendCount = 1
// makes a friend...
now friendCount = 2let friendCount = 1;
// makes a friend...
friendCount = 2;
Reassigning or updating a variable requires the now keyword.
This makes it more clear when you are reassigning
(which shouldn't be often), and enables the const-by-default syntax.
let isDarkSide = false
// ... gets stuck in traffic ...
now isDarkSide = truelet isDarkSide = false;
// ... gets stuck in traffic ...
isDarkSide = true;
Assignments that update a variable also require the now keyword:
let planetsDestroyed = 0
// the death star is fully operational
now planetsDestroyed += 1let planetsDestroyed = 0;
// the death star is fully operational
planetsDestroyed += 1;
However, assigning to an object's property allows, but does not require now:
now milleniumFalcon.kesselRun = 'less than 12 parsecs!'
milleniumFalcon.kesselRun = 'less than 12 parsecs!'milleniumFalcon.kesselRun = "less than 12 parsecs!";
milleniumFalcon.kesselRun = "less than 12 parsecs!";
Similarly, non-assignment updates allow but do not currently require now:
now planetsDestroyed++
planetsDestroyed++planetsDestroyed++;
planetsDestroyed++;
You cannot shadow a variable using const shorthand:
let myVar = 2
if (true) {
myVar = 3
}unknown: `myVar` is shadowed from a higher scope. If you want to reassign the variable, use `now myVar = ...`. If you want to declare a new shadowed `const` variable, you must use `const myVar = ...` explicitly.
1 | let myVar = 2
2 | if (true) {
> 3 | myVar = 3
| ^
4 | }The above is not allowed because it looks ambiguous and confusing;
did you mean to declare a new variable, or were you trying to update
the existing variable, and just forgot to use now?
Instead, you must explictly use either now or const:
let myVar = 2
if (true) {
const myVar = 3
}
myVar === 2 // true
let myOtherVar = 4
if (true) {
now myOtherVar = 5
}
myOtherVar === 5 // truelet myVar = 2;
if (true) {
const myVar = 3;
}
myVar === 2; // true
let myOtherVar = 4;
if (true) {
myOtherVar = 5;
}
myOtherVar === 5; // true
Since (almost) all valid JavaScript is valid LightScript, you can write loops and conditionals as you always would, complete with parens and curlies.
You can write if, for, etc without parens, but with curly braces:
for const x of arr {
if x > 10 {
print('wow, so big!')
} else {
print('meh')
}
}for (const x of arr) {
if (x > 10) {
print("wow, so big!");
} else {
print("meh");
}
}
Or, if you prefer, use significant indentation:
for const x of arr:
if x > 10:
print('wow, so big!')
else:
print('meh')for (const x of arr) {
if (x > 10) {
print("wow, so big!");
} else {
print("meh");
}
}
This "curly-or-whitespace" option is also available for class and function bodies.
Whitespace (significant indentation) is often more concise and readable for simple code, but when blocks get very long or deeply nested, curly braces offer better visibility. Teams have the freedom to choose a style that feels comfortable.
Blocks that contain only a single statement can be written on the same line:
for elem thing in list: print(thing)
if x < 10: print('x is small')for (let _i = 0, _len = list.length; _i < _len; _i++) {
const thing = list[_i];
print(thing);
}
if (x < 10) print("x is small");
One-line blocks can be combined:
for elem x in arr: if x < 10: print(x)for (let _i = 0, _len = arr.length; _i < _len; _i++) {
const x = arr[_i];
if (x < 10) print(x);
}
You cannot, however, mix one-line and multiline syntax:
for x of arr: print(x)
print('This is broken.')unknown: for-of requires a variable qualifier: `now` to reassign an existing variable, or `const`, `let`, `var` to declare a new one. (1:4)
> 1 | for x of arr: print(x)
| ^
2 | print('This is broken.')An indent is two spaces, period.
LightScript only allows "normal" (ascii-32) whitespace;
tabs, non-breaking spaces, and other invisible characters raise a SyntaxError.
Similarly, only \n and \r\n are valid line terminators.
Overindentation is currently allowed, but discouraged. It may be made illegal in the future.
An indented block is parsed until the indent level of a line is less than or equal to
the indent level of the line that started the block (the line with a : or ->).
For example, this does not work:
if treeIsPretty and
treeIsTall:
climbTree()unknown: Expected an Indent or Statement (2:12)
1 | if treeIsPretty and
> 2 | treeIsTall:
| ^
3 | climbTree()but this does (because the line with the : has one indent):
if treeIsPretty and
treeIsTall:
climbTree()if (treeIsPretty && treeIsTall) {
climbTree();
}
However, that's a little ugly;Â the recommended style is:
if (
treeIsPretty and
treeisTall
):
climbTree()if (treeIsPretty && treeisTall) {
climbTree();
}
While LightScript allows both "curly blocks" and significantly-indented blocks, they cannot be mixed for the same construct:
if true:
okay()
else {
thisIsNotAllowed()
}unknown: Unexpected token, expected : (3:4)
1 | if true:
2 | okay()
> 3 | else {
| ^
4 | thisIsNotAllowed()
5 | }Furthermore, the indentation level must be the same across all branches of
if/else/elif, try/catch/finally and do/while.
elifThe same as else if, which you can also use:
if awesome:
jumpForJoy()
elif great:
highFive()
else if good:
smileMeekly()
else:
cringe()if (awesome) {
jumpForJoy();
} else if (great) {
highFive();
} else if (good) {
smileMeekly();
} else {
cringe();
}
if expressionsIn LightScript, ternaries look like ifs:
animal = if canBark: 'dog' else: 'cow'const animal = canBark ? "dog" : "cow";
if expressionsanimal =
if canBark:
'dog'
elif canMeow:
print('These ternaries can take multiple expressions')
'cat'
else:
'cow'const animal = canBark
? "dog"
: canMeow
? (print(
"These ternaries can take multiple expressions"
), "cat")
: "cow";
Note that if you move the if to the first line, the rest of the code
must be dedented so that the elses have the same indent level as animal.
null-default if expressionsIf you don't include an else, it will be null:
maybeDog = if canBark: 'dog'const maybeDog = canBark ? "dog" : null;
==1 == 11 === 1;
Both == and === compile to ===, which is almost always what you want.
When you actually want to use a loose-equals (==), call the looseEq() function
from the standard library (eg; 1~looseEq('1')).
!=1 != 01 !== 0;
Similarly, both != and !== compile to !==.
When you actually want to use a loose-not-equals (!=),
call the looseNotEq() function from the standard library.
ora or ba || b;
anda and ba && b;
notnot c!c;
not may be removed from the language in the future.
JavaScript has half a dozen ways to define a function; LightScript unifies that to just one, consistent across contexts.
The basic syntax comes from stripping down the fat arrow:
const myFunction = (x, y) => x + yconst myFunction = (x, y) => x + y;
to the more minimal:
myFunction(x, y) => x + yconst myFunction = (x, y) => x + y;
Unbound functions use a skinny arrow (->),
async functions use a barbed arrow (-/> or =/>),
and methods look the exact same as top-level functions.
LightScript functions have implicit returns and optional curly braces:
myFunction(x, y) =>
print('multiplying is fun!')
x * y
myCurlyFunction(x, y) => {
print('adding is fun!')
x + y
}const myFunction = (x, y) => {
print("multiplying is fun!");
return x * y;
};
const myCurlyFunction = (x, y) => {
print("adding is fun!");
return x + y;
};
foo() => this.somePropvar _this = this;
const foo = () => _this.someProp;
Compiles to ES6 fat arrows whenever possible,
and inserts the relevant .bind() call otherwise.
See also bound methods.
Note that when used in an expression, the name is discarded:
runCallback(foo() => 1)runCallback(() => 1);
Skinny arrows (->) are unbound:
ultimateQuestion() -> 6 * 9
sillySumPlusTwo(a, b) ->
now a++
now b++
a + bfunction ultimateQuestion() {
return 6 * 9;
}
function sillySumPlusTwo(a, b) {
a++;
b++;
return a + b;
}
While you're welcome to use => pretty much everywhere, there are a few advantages
of using skinny arrows:
function declarations are hoisted, meaning you can declare utility methods
at the bottom of a file, and main methods at the top..bind() calls, which may be unncessary if the method
doesn't actually need to be bound.To disable implicit returns for a method, give it a void type annotation.
foo(): void ->
1function foo(): void {
1;
}
LightScript does not add implicit returns:
void returnType annotation (eg; fn(): void ->).{ prop(newValue) -set> this._prop = newValue }).constructor() ->), which generaly should not return.LightScript uses Facebook's Flow typechecker and type syntax.
foo(a: string, b: number): number ->
a.length + bfunction foo(a: string, b: number): number {
return a.length + b;
}
Polymorphic:
foo<T>(a: T): T -> afunction foo<T>(a: T): T {
return a;
}
runCallback(() -> 42)
runCallback(param -> param * 2)
runCallback(param => param * 2)runCallback(function() {
return 42;
});
runCallback(function(param) {
return param * 2;
});
runCallback(param => param * 2);
foo() -/>
Promise.resolve(42)
boundFoo() =/>
Promise.resolve(this.answer + 42)var _this = this;
async function foo() {
return Promise.resolve(42);
}
const boundFoo = async () => {
return Promise.resolve(_this.answer + 42);
};
See also await.
foo() -*>
yield 3
yield 4function* foo() {
yield 3;
return yield 4;
}
Note that JavaScript does not support fat-arrow generator functions; LightScript compiles them to bound functions:
boundFoo() =*>
yield 3
yield 4function* boundFoo() {
yield 3;
return yield 4;
}
boundFoo = boundFoo.bind(this);
If you are using the
async-generator-functions babel transform,
you can define async generators with -*/> and =*/>.
Note that this is (at time of writing) a stage 3 proposal
and thus not yet part of babel-preset-env or any browers.
obj = {
foo() -> 'hello'
bar() ->
'hi there'
}const obj = {
foo() {
return "hello";
},
bar() {
return "hi there";
}
};
obj = {
name: 'Jack'
loudName() => this.name.toUpperCase()
}const obj = {
name: "Jack",
loudName() {
return this.name.toUpperCase();
}
};
obj.loudName = obj.loudName.bind(obj);
obj = {
foo() -get> this._foo
foo(newValue) -set> this._foo = newValue
}
obj.foo = 'hi'
obj.fooconst obj = {
get foo() {
return this._foo;
},
set foo(newValue) {
this._foo = newValue;
}
};
obj.foo = "hi";
obj.foo;
See also Classes.
Note that fat arrows (=get> and =set>) are not available,
as getters and setters generally do not require binding.
Note also that -get> and -set> cannot be combined with -/> or -*> syntax.
getData(url) -/>
response <- fetch(url)
<- response.json()async function getData(url) {
const response = await fetch(url);
return await response.json();
}
To assign to a const, supply a variable name on the left side of the arrow:
getData(url) -/>
response <- fetch(url)
responseasync function getData(url) {
const response = await fetch(url);
return response;
}
To reassign an existing variable, use now:
reassignData(data) -/>
now data <- asyncTransform(data)
dataasync function reassignData(data) {
data = await asyncTransform(data);
return data;
}
If you are mutating an object's property, now is optional:
reassignDataProp(obj) -/>
now obj.data <- process(obj.data)
obj.data <- process(obj.data)
objasync function reassignDataProp(obj) {
obj.data = await process(obj.data);
obj.data = await process(obj.data);
return obj;
}
Note that in all cases, the <- must be on the same line as the variable.
A <- that begins a line is a "naked await":
delayed(action, delay) -/>
<- waitFor(delay)
action()async function delayed(action, delay) {
await waitFor(delay);
return action();
}
It can be implicitly returned like anything else:
getData(url) =/>
response <- fetch(url)
<- response.json()const getData = async url => {
const response = await fetch(url);
return await response.json();
};
When an await is followed by a [, it is wrapped in Promise.all():
fetchBoth(firstUrl, secondUrl) -/>
<- [fetch(firstUrl), fetch(secondUrl)]async function fetchBoth(firstUrl, secondUrl) {
return await Promise.all([
fetch(firstUrl),
fetch(secondUrl)
]);
}
You do not need to use the <- symbol to take advantage of this:
fetchBoth(firstUrl, secondUrl) -/>
return await [fetch(firstUrl), fetch(secondUrl)]async function fetchBoth(firstUrl, secondUrl) {
return await Promise.all([
fetch(firstUrl),
fetch(secondUrl)
]);
}
You cannot pass a value that happens to be an array; it must be contained in []:
awaitAll(promises) -/>
<- promisesasync function awaitAll(promises) {
return await promises;
}
doesn't work, but this does:
awaitAll(promises) -/>
<- [...promises]async function awaitAll(promises) {
return await Promise.all([...promises]);
}
This can alo be combined with Array Comprehensions:
fetchAll(urls) -/>
<- [for elem url in urls: fetch(url)]async function fetchAll(urls) {
return await Promise.all(
(() => {
const _arr = [];
for (
let _i = 0, _len = urls.length;
_i < _len;
_i++
) {
const url = urls[_i];
_arr.push(fetch(url));
}
return _arr;
})()
);
}
The most likely source of errors in any application should occur at I/O boundaries,
which are also typically crossed asynchronously. Any time you fetch() across a network,
you should expect it to fail some percentage of the time, and prepare accordingly.
In JavaScript, this can be inconvenient:
getData(url) -/>
let response
try {
now response = await fetch(url)
} catch (err) {
handle(err)
return
}
return await response.json()async function getData(url) {
let response;
try {
response = await fetch(url);
} catch (err) {
handle(err);
return;
}
return await response.json();
}
The small try/catch blocks are annoying and force you to use unnecessary lets.
The alternative is to put all logic using the awaited value into the try block,
which is also an anti-pattern.
In LightScript, you can easily wrap an await in a try/catch:
getData(url) -/>
response <!- fetch(url)
if isError(response):
handle(response)
return
<- response.json()import isError from "lodash/isError";
async function getData(url) {
const response = await (async () => {
try {
return await fetch(url);
} catch (_err) {
return _err;
}
})();
if (isError(response)) {
handle(response);
return;
}
return await response.json();
}
This pattern is much closer to the "safely handle errors within normal control-flow"
philosophy of Rust, Haskell, and Go, which use Result, Maybe, or multiple return values.
Because LightScript uses Flow for static type checking, any code that follows a
<!- must handle the error case.
(also known as safe navigation operator, optional chaining operator, safe call operator, null-conditional operator)
lacesOrNull = hikingBoots?.lacesconst lacesOrNull = hikingBoots == null
? null
: hikingBoots.laces;
This also works with computed properties:
treeTrunk?.rings?[age](treeTrunk == null ? null : treeTrunk.rings) == null
? null
: treeTrunk.rings[age];
Safe chains that contain methods will not be called more than once:
getDancingQueen()?.feelTheBeat(tambourine)
getDanceFloor().dancingQueen?.isSeventeenvar _ref, _ref2;
(_ref = getDancingQueen()) == null
? null
: _ref.feelTheBeat(tambourine);
(_ref2 = getDanceFloor().dancingQueen) == null
? null
: _ref2.isSeventeen;
Note that the default value is null, unlike CoffeeScript's undefined.
firstChance = chances.0
secondChance = chances.1const firstChance = chances[0];
const secondChance = chances[1];
This is a minor feature to make chaining more convenient, and may be removed in the future.
There is not a negative index feature (eg; chances.-1 doesn't work),
but with the standard library you can write:
lastChance = chances~last()import last from "lodash/last";
const lastChance = last(chances);
This uses the Tilde Call feature and the lodash.last() function,
which LightScript makes available in its standard library.
You can define properties that are functions using the standard LightScript arrow syntax:
mammaMia.resistYou() -> false
mammaMia.hereWeGo() => this.again
Mamma.prototype.name() -> "Mia!"mammaMia.resistYou = function resistYou() {
return false;
};
mammaMia.hereWeGo = (function hereWeGo() {
return this.again;
}).bind(mammaMia);
Mamma.prototype.name = function name() {
return "Mia!";
};
This is a headline feature of LightScript, and a slightly unique mix of Kotlin's Extensions Methods, Ruby's Monkey Patching, and Elixir's Pipelines.
subject~verb(object)verb(subject, object);
The underlying goal is to encourage the functional style of separating immutable typed records from the functions that go with them, while preserving the human-readability of "subject.verb(object)" syntax that Object-Oriented methods provide.
It enables slightly more readable code in simple situations:
if response~isError():
freakOut()import isError from "lodash/isError";
if (isError(response)) {
freakOut();
}
money = querySelectorAll('.money')
money~map(mustBeFunny)import map from "lodash/map";
const money = querySelectorAll(".money");
map(money, mustBeFunny);
And makes chaining with functions much more convenient, obviating intermediate variables:
allTheDucks
.map(duck => fluffed(duck))
~uniq()
~sortBy(duck => duck.height)
.filter(duck => duck.isGosling)import uniq from "lodash/uniq";
import sortBy from "lodash/sortBy";
sortBy(
uniq(allTheDucks.map(duck => fluffed(duck))),
duck => duck.height
).filter(duck => duck.isGosling);
Note that all lodash methods are included in the LightScript standard library.
See also Methods.
The same as JavaScript (ES7):
obj = { a: 'a', b, [1 + 1]: 'two' }const obj = { a: "a", b, [1 + 1]: "two" };
For all ES7 features, use babel-preset-lightscript instead of babel-plugin-lightscript
or include the babel plugins directly.
Commas are optional; newlines are preferred.
obj = {
a: 'a'
b
method() =>
3
[1 + 1]: 'two'
}const obj = {
a: "a",
b,
method() {
return 3;
},
[1 + 1]: "two"
};
obj.method = obj.method.bind(obj);
The same as JavaScript.
arr = [1, 2, 3]const arr = [1, 2, 3];
Again, commas are optional; newlines are preferred.
arr = [
1
2
2 + 1
5 - 1
5
]const arr = [1, 2, 2 + 1, 5 - 1, 5];
In JavaScript, there's really only one fast option for iteration: for-;;.
It's so ugly, though, that most developers avoid it in favor of more ergonomic
(but less performant and powerful) alternatives, like .forEach() and for-of.
With LightScript, you don't have to compromise.
Iterate over indices and elements of an array:
for idx i, elem x in arr:
print(i, x)for (let i = 0, _len = arr.length; i < _len; i++) {
const x = arr[i];
print(i, x);
}
Only indices:
for idx i in arr:
print(i)for (let i = 0, _len = arr.length; i < _len; i++) {
print(i);
}
Only elements:
for elem x in arr:
print(x)for (let _i = 0, _len = arr.length; _i < _len; _i++) {
const x = arr[_i];
print(x);
}
Note that if you are iterating over something more complicated than a variable (eg; a function call), it will be lifted into its own variable so as not to be called twice:
for elem x in foo():
print(x)for (
let _arr = foo(), _i = 0, _len = _arr.length;
_i < _len;
_i++
) {
const x = _arr[_i];
print(x);
}
Iterate over keys and values of an object:
for key k, val v in obj:
print(k, v)for (
let _i = 0, _keys = Object.keys(obj), _len = _keys.length;
_i < _len;
_i++
) {
const k = _keys[_i];
const v = obj[k];
print(k, v);
}
Only keys:
for key k in obj:
print(k)for (
let _i = 0, _keys = Object.keys(obj), _len = _keys.length;
_i < _len;
_i++
) {
const k = _keys[_i];
print(k);
}
Only values:
for val v in obj:
print(v)for (
let _i = 0, _keys = Object.keys(obj), _len = _keys.length;
_i < _len;
_i++
) {
const _k = _keys[_i];
const v = obj[_k];
print(v);
}
Note the use of Object.keys() under the hood, as this will only iterate over
own keys, not inherited ones. Use a traditional for-in
if you wish to iterate over inherited properties as well.
There is no builtin support for ranges.
When the object instantiation is acceptable, use of Array()
or the lodash range() method (provided by the stdlib)
is recommended:
for idx i in Array(10):
print(i)for (
let _arr = Array(10), i = 0, _len = _arr.length;
i < _len;
i++
) {
print(i);
}
for idx i in range(20, 100, 2):
print(i)import range from "lodash/range";
for (
let _arr = range(20, 100, 2), i = 0, _len = _arr.length;
i < _len;
i++
) {
print(i);
}
If you are performing a numerical iteration and the array instantiation is problematic
for performance, for-;; is recommended:
for let i = 0; i < n; i++:
print(i)for (let i = 0; i < n; i++) {
print(i);
}
You can destructure the elements of an Array or the values of an Object, similar to ES2015 JavaScript:
Array element destructuring:
for elem { color } in [{ color: 'blue' }]:
print(color)for (
let _arr = [{ color: "blue" }],
_i = 0,
_len = _arr.length;
_i < _len;
_i++
) {
const { color } = _arr[_i];
print(color);
}
Object value destructuring:
for val [first, second] in { bases: ['who', 'what'] }:
print(first, second)for (
let _obj = { bases: ["who", "what"] },
_i = 0,
_keys = Object.keys(_obj),
_len = _keys.length;
_i < _len;
_i++
) {
const _k = _keys[_i];
const [first, second] = _obj[_k];
print(first, second);
}
The full power of destructuring syntax is possible here:
for elem { props: { color, size: [w, h] } } in arr:
print(color, w, h)for (let _i = 0, _len = arr.length; _i < _len; _i++) {
const { props: { color, size: [w, h] } } = arr[_i];
print(color, w, h);
}
for-inIf you wish to iterate over all owned and inherited keys of an object,
use for-in with const, let, var, or now:
for const k in obj:
print(k)for (const k in obj) {
print(k);
}
var k;
for now k in {a: 1, b: 2}:
print()
print(k)
// "b"var k;
for (k in { a: 1, b: 2 }) {
print();
}
print(k);
// "b"
Unfortunately, the more concise for x in arr form would be ambiguous
(is it keys of an object or values of an array?) and is not allowed:
for x in arr:
print(x)unknown: for-in requires a variable qualifier: `now` to reassign an existing variable, or `const`, `let`, `var` to declare a new one. Use `idx` or `elem` to iterate an array. Use `key` or `val` to iterate an object. (1:4)
> 1 | for x in arr:
| ^
2 | print(x)for-ofIf you are iterating over [Symbol.iterator] (eg; a generator function),
use for-of as in JS.
A construct like for iter x in gen may be introduced in the future for consistency.
Like for-in, for-of must include const, let, var, or now:
for const x of gen:
print(gen)for (const x of gen) {
print(gen);
}
A naked variable is not allowed:
for x of gen():
print(x)unknown: for-of requires a variable qualifier: `now` to reassign an existing variable, or `const`, `let`, `var` to declare a new one. (1:4)
> 1 | for x of gen():
| ^
2 | print(x)This is to encourage developers iterating over arrays to use the more performant
for elem x in arr rather than for x of arr, which is slower.
It may be relaxed in the future.
forfor elem x in stuff: print(x)for (let _i = 0, _len = stuff.length; _i < _len; _i++) {
const x = stuff[_i];
print(x);
}
This syntax can be used with all for loops.
Note that you can combine this with single-line if statements:
for elem x in stuff: if x > 3: print(x)for (let _i = 0, _len = stuff.length; _i < _len; _i++) {
const x = stuff[_i];
if (x > 3) print(x);
}
while loopsAs in JavaScript, with the standard syntax options:
while true:
doStuff()while (true) {
doStuff();
}
do-whileAs in JavaScript, with the standard syntax options:
do:
activities()
while truedo {
activities();
} while (true);
A newline (or semicolon) must follow the while clause, so this is not legal in LightScript:
do:
activities()
while (true) foo()unknown: Unexpected token, expected ; (3:13)
1 | do:
2 | activities()
> 3 | while (true) foo()
| ^switchAs in JavaScript. Curly braces around the cases are required;
parens around the discriminant are not:
switch val {
case "x":
break
case "y":
break
}switch (val) {
case "x":
break;
case "y":
break;
}
This may change in the future. A guard or match feature may also be added.
doubledItems =
[ for elem item in array: item * 2 ]const doubledItems = (() => {
const _arr = [];
for (let _i = 0, _len = array.length; _i < _len; _i++) {
const item = array[_i];
_arr.push(item * 2);
}
return _arr;
})();
filteredItems =
[ for elem item in array: if item > 3: item ]const filteredItems = (() => {
const _arr = [];
for (let _i = 0, _len = array.length; _i < _len; _i++) {
const item = array[_i];
if (item > 3) _arr.push(item);
}
return _arr;
})();
Note that you can nest for-loops within an array, and they can take up multiple lines:
listOfPoints = [
for elem x in xs:
for elem y in ys:
if x and y:
{ x, y }
]const listOfPoints = (() => {
const _arr = [];
for (let _i = 0, _len = xs.length; _i < _len; _i++) {
const x = xs[_i];
for (
let _i2 = 0, _len2 = ys.length;
_i2 < _len2;
_i2++
) {
const y = ys[_i2];
if (x && y) {
_arr.push({ x, y });
}
}
}
return _arr;
})();
You can also nest comprehensions within comprehensions for constructing multidimensional arrays:
matrix = [ for idx row in Array(n):
[ for idx col in Array(n): { row, col } ]
]const matrix = (() => {
const _arr = [];
for (
let _arr2 = Array(n), row = 0, _len = _arr2.length;
row < _len;
row++
) {
_arr.push(
(() => {
const _arr3 = [];
for (
let _arr4 = Array(n),
col = 0,
_len2 = _arr4.length;
col < _len2;
col++
) {
_arr3.push((row, col));
}
return _arr3;
})()
);
}
return _arr;
})();
Note that if else is not provided, items that do not match an if are filtered out; that is,
[ for idx i in Array(5): if i > 2: i ](() => {
const _arr = [];
for (
let _arr2 = Array(5), i = 0, _len = _arr2.length;
i < _len;
i++
) {
if (i > 2) _arr.push(i);
}
return _arr;
})();
will result in [3, 4], not [null, null, null, 3, 4]
As with Array Comprehensions, but wrapped in {} and with comma-separated key, value.
{ for elem item in array: (item, f(item)) }(() => {
const _obj = {};
for (let _i = 0, _len = array.length; _i < _len; _i++) {
const item = array[_i];
_obj[item] = f(item);
}
return _obj;
})();
The parens are optional:
flipped =
{ for key k, val v in obj: v, k }const flipped = (() => {
const _obj = {};
for (
let _i = 0,
_keys = Object.keys(obj),
_len = _keys.length;
_i < _len;
_i++
) {
const k = _keys[_i];
const v = obj[k];
_obj[v] = k;
}
return _obj;
})();
class Animal {
talk() -> 'grrr'
}
class Person extends Animal:
talk() -> 'hello!'class Animal {
talk() {
return "grrr";
}
}
class Person extends Animal {
talk() {
return "hello!";
}
}
class Clicker extends Component:
handleClick(): void =>
this.setState({ clicked: true })
render() ->
<button onClick={this.handleClick}>
Click me!
</button>class Clicker extends Component {
constructor(..._args) {
super(..._args);
this.handleClick = this.handleClick.bind(this);
}
handleClick(): void {
this.setState({ clicked: true });
}
render() {
return (
<button onClick={this.handleClick}>
Click me!
</button>
);
}
}
Use a fat arrow (=>) to bind class methods.
This will be added to the constructor after a super call, which will be created if it does not exist.
You cannot use bound class methods if you return super(),
which is something you typically shouldn't do anyway.
If you define bound class methods with =>, a constructor method will be inserted
if one did not already exist.
LightScript will also insert super() for you if a constructor is defined
in a class that extends another class, and will pass along your parameters
to the base class.
class Person extends Animal:
constructor(foo) ->
console.log("I forgot to call super!")class Person extends Animal {
constructor(foo) {
super(foo);
console.log("I forgot to call super!");
}
}
To disable super-insertion, define the constructor without a LightScript arrow:
class NaughtyPerson extends Person:
constructor(foo) {
console.log("I don't want super called!")
}class NaughtyPerson extends Person {
constructor(foo) {
console.log("I don't want super called!");
}
}
class Animal:
static kingdom() => this.nameclass Animal {
static kingdom() {
return this.name;
}
}
Animal.kingdom = Animal.kingdom.bind(Animal);
In this example, kingdom() will always return 'Animal',
regardless of its calling context.
class Animal:
noise() -get>
this.sound or 'grrr'
noise(newValue) -set>
this.sound = newValueclass Animal {
get noise() {
return this.sound || "grrr";
}
set noise(newValue) {
this.sound = newValue;
}
}
See also Object Methods.
As in ES7:
class Animal:
noise = 'grrr'class Animal {
noise = "grrr";
}
Note that babel-plugin-lightscript by itself will not process class properties;
you must include the babel-plugin-transform-class-properties plugin yourself,
or use babel-preset-lightscript.
As in ES7:
class Animal:
static isVegetable = false
static canMakeNoise() -> trueclass Animal {
static isVegetable = false;
static canMakeNoise() {
return true;
}
}
As in ES7:
@classDecorator
class Animal:
@methodDecorator
talk() -> 'grrr'@classDecorator class Animal {
@methodDecorator talk() {
return "grrr";
}
}
Note that babel-plugin-lightscript by itself will not process decorators;
you must include the babel-plugin-transform-decorators-legacy plugin yourself,
or use babel-preset-lightscript.
By default, LightScript makes all of Lodash available to be imported as needed:
[0.1, 0.3, 0.5, 0.7]~map(round)~uniq()
// [0, 1]import map from "lodash/map";
import round from "lodash/round";
import uniq from "lodash/uniq";
uniq(map([0.1, 0.3, 0.5, 0.7], round));
// [0, 1]
There are also several non-Lodash functions available which will be inlined:
looseEq(3, '3')
// true
2~looseNotEq('3')
// true
bitwiseNot(1)
// -2function looseEq(a, b) {
return a == b;
}
function looseNotEq(a, b) {
return a != b;
}
function bitwiseNot(a) {
return ~a;
}
looseEq(3, "3");
// true
looseNotEq(2, "3");
// true
bitwiseNot(1);
// -2
Every method in Lodash v4
npm install --save lodash if you wish to use these features.looseEq(a, b), which uses the JavaScript loose-equality == to compare two variables
(available because in LightScript, == compiles to ===).looseNotEq(a, b), which uses the JavaScript loose-inequality != to compare two variables.all the JavaScript bitwise operators
bitwiseNot(x), returns the result of ~x (since ~ has been repurposed in LightScript for Tilde Calls).bitwiseAnd(a, b), returns the result of a & b.bitwiseOr(a, b), returns the result of a | b.bitwiseXor(a, b), returns the result of a ^ b.bitwiseLeftShift(a, b), returns the result of a << b.bitwiseRightShift(a, b), returns the result of a >> b.bitwiseZeroFillRightShift(a, b), returns the result of a >>> b.User-defined identifiers will override a stdlib identifier:
round(x) -> 100
[0.1, 0.3, 0.5, 0.7]~map(round)~uniq()
// [100]import map from "lodash/map";
import uniq from "lodash/uniq";
function round(x) {
return 100;
}
uniq(map([0.1, 0.3, 0.5, 0.7], round));
// [100]
In the future, this will be discouraged with an ESLint rule.
To disable this feature, pass stdlib: false to babel-plugin-lightscript, eg;
// .babelrc
{
"plugins": [
["lightscript", { "stdlib": false }]
]
}// .babelrc
({
plugins: [["lightscript", { stdlib: false }]]
});
You may also similarly disable inclusion of lodash:
// .babelrc
{
"plugins": [
["lightscript", {
"stdlib": {
"lodash": false,
}
}]
]
}// .babelrc
({
plugins: [
[
"lightscript",
{
stdlib: {
lodash: false
}
}
]
]
});
Note that this is unlikely to be necessary; projects that simply don't call lodash methods won't have any lodash imports, and if you do the import yourself LightScript won't add an import.
If you are not transpiling import to require() calls with another plugin
(or with babel-preset-lightscript), you may need the require: true setting:
// .babelrc
{
"plugins": [
["lightscript", {
"stdlib": {
"require": true,
}
}]
]
}// .babelrc
({
plugins: [
[
"lightscript",
{
stdlib: {
require: true
}
}
]
]
});
See the tl;dr for a quick overview
90% of the time, JavaScript's Automatic Semicolon Insertion feature works every time. That is, in most JavaScript code, semicolons are unnecessary.
But there are a handful
of cases where a semicolon needs to be inserted, as encoded in
the eslint semi: "never" rule:
statements beginning with
[,(,/,+, or-
ES6 and JSX each introduce an additional ambiguity: ` and <, which are handled as well.
LightScript solves each issue in a slightly different way, though each fix is essentially an encoding of stylistic best-practice into the syntax itself.
In practice, if you stick to community-standard code style, you should not have to worry about any of this; they are documented for completeness.
+ and -: binary vs. unaryThere are two possible interpretations of this code:
1
-11;
-1;
It could either be a 1 statement followed by a -1 (negative one)
statement, or a single 1 - 1 statement.
JavaScript chooses 1 - 1, which is typically undesired.
This is because + and - take two forms in JavaScript:
binary (add or subtract two numbers) and unary
(make a number positive or negative).
To resolve this ambiguity, LightScript requires that unary + and -
are not separated by a space from their argument.
That is, -1 is "negative one" while - 1 is invalid LightScript.
With this restriction in place, it easy to give preference to the unary form
when a + or - begins a line:
1
+ 1
1+1
1
+11 + 1;
1 + 1;
1;
+1;
Only the last example is a deviation from JavaScript,
which would interpret the two lines as 1 + 1.
Again, beware that unary + and - cannot be followed by a space in LightScript:
- 1unknown: Unary +/- cannot be followed by a space in lightscript. (1:0)
> 1 | - 1
| ^Without this fix, it would be difficult to implicitly return negative numbers:
negativeThree() ->
three = 3
-threefunction negativeThree() {
const three = 3;
return -three;
}
(This would be const three = 3 - three; in JavaScript).
Similarly, it would be difficult to have lists with negative numbers:
numbers = [
0
-1
-2
]const numbers = [0, -1, -2];
(Without this ASI fix, that'd be const numbers = [0 - 1 - 2];).
/: division vs. regular expressionIn JavaScript, the following code throws a SyntaxError:
let one = 1
/\n/.test('1')let one = 1;
/\n/.test("1");
This is because it tries to parse the / at the start of the second line as division.
As you can see, LightScript does not share this problem.
LightScript makes a slightly crude generalization that draws from the same strategy
as + and - (above): Regular Expressions can't start with a space character ():
/ \w/.test(' broken')unknown: Regex literals cannot start with a space in lightscript; try '\s' or '\ ' instead. (1:1)
> 1 | / \w/.test(' broken')
| ^This doesn't happen very often, and when it does, can be trivially fixed
by escaping the space or using a \s character:
/\ \w/.test(' not broken')
/\s\w/.test(' not broken')/\ \w/.test(" not broken");
/\s\w/.test(" not broken");
Similary, a division / that starts a line cannot be followed by a space:
1
/2unknown: Unterminated regular expression (if you wanted division, add a space after the '/'). (2:1)
1 | 1
> 2 | /2
| ^This space is not required when the / does not start a line:
1/2
1
/ 21 / 2;
1 / 2;
(: expressions vs. function callsThis is perhaps the most frequently problematic ASI failure, and the most easily fixed.
In JavaScript, the following code would try to call one(1 + 1), which is not what you want:
two = one + one
(1 + 1) / 3const two = one + one;
(1 + 1) / 3;
In LightScript, the opening paren of a function call must be on the same line as the function:
doSomething(
param)
doSomething (
param
)
doSomething
(param) // oops!doSomething(param);
doSomething(param);
doSomething;
param; // oops!
[: index-access vs. arraysIn JavaScript, the following code would try to access one[1, 2, 3] which isn't what you want:
two = one + one
[1, 2, 3].forEach(i => console.log(i))const two = one + one;
[1, 2, 3].forEach(i => console.log(i));
That's because you often do see code like this:
firstChild = node
.children
[0]const firstChild = node.children[0];
In LightScript, accessing an index or property using [] requires an indent:
node
.children
[0]
node
.children
[0] // oops!unknown: Indentation required. (6:0)
4 |
5 | node
> 6 | .children
| ^
7 | [0] // oops!The required indent is relative to the line that starts a subscript chain.
Note that this rule also applies to the "numerical index access" feature:
node
.children
.0
node
.children
.0 // oops!unknown: Indentation required. (6:0)
4 |
5 | node
> 6 | .children
| ^
7 | .0 // oops!<: less-than vs. JSXIn JavaScript, the following would be parsed as one < MyJSX and break:
two = one + one
<MyJSX />const two = one + one;
<MyJSX />;
LightScript solves this in a similar manner to +, -, and /:
a less-than < that starts a line must be followed by a space:
isMyNumberBig = bigNumber
<myNumberunknown: Unexpected token (2:11)
1 | isMyNumberBig = bigNumber
> 2 | <myNumber
| ^is broken, but this works:
isMyNumberBig = bigNumber
< myNumberconst isMyNumberBig = bigNumber < myNumber;
`: tagged vs. untagged templatesIn JavaScript, the following would be parsed as hello`world`:
hello
`world`hello;
`world`;
As with function calls, a tagged template expression in LightScript must have
the opening ` on the same line as the tag.
You never need semicolons in LightScript to separate statements.
Instead, there are a few restrictions around edge cases:
+1 and -1 instead of + 1 and - 1./\ / or /\s/, not / /.+, -, \, or <, the symbol must be followed by a space:isOver100 = twoHundred
/ four
+ oneHundred
- fifty
< myNumberconst isOver100 = twoHundred / four + oneHundred - fifty <
myNumber;
Unfortunately, there are a few ambiguous corner-cases. You are unlikely to hit them and there are easy fixes.
If you have an if whose test is a function call,
and whose consequent is an arrow function without parentheses or curly braces, eg;
if fn(): x => 4unknown: Paren-free test expressions must be followed by braces or a colon. (1:15)
> 1 | if fn(): x => 4
| ^it will parse as a function fn() => 4 with type annotation x,
and then throw a SyntaxError: Unexpected token, expected :.
This can be corrected by wrapping the param in parens:
if fn(): (x) => 4unknown: Paren-free test expressions must be followed by braces or a colon. (1:17)
> 1 | if fn(): (x) => 4
| ^LightScript is a "rough superset" of JavaScript: almost all valid JavaScript is valid LightScript.
This section aims to comprehensively document the cases where valid JavaScript compiles differently (or breaks) in LightScript.
Most cases have been covered elsewhere in this documentation, but are grouped here for convenience.
now, or, and, and not are reserved words in LightScript.
== and !=Perhaps the biggest semantic change, == compiles to === and != compiles to !==.
All bitwise operators have been removed. The unary Bitwise NOT ~ has been repurposed
for Tilde Calls.
Instead you may use the replacements (like bitwiseNot(x)) provided by the standard library.
Bitwise assignment operators (|=, &=, ^=, <<=, >>=, >>>=)
remain but may be removed as well in the future.
See ASI for a handful of breaking syntax changes, mainly requiring or disallowing a space after an operator.
While in JavaScript, the following is valid:
do {} while (x) foo()unknown: Unexpected token, expected ; (1:16)
> 1 | do {} while (x) foo()
| ^It is illegal in LightScript. A ; or newline must follow the while conidition,
regardless of whether parens are used.
if, while, switch, and withWhile in JavaScript, the following are valid:
if
(condition) {
result()
}
while
(notDone())
doStuff()unknown: Illegal newline. (1:2)
> 1 | if
| ^
2 | (condition) {
3 | result()
4 | }They are illegal in LightScript. Instead, put the condition on the same line as the if
or use parens like so:
if (
condition and
otherCondition
):
result()
while (
notDone() and
notBoredYet()
):
doStuff()import result from "lodash/result";
if (condition && otherCondition) {
result();
}
while (notDone() && notBoredYet()) {
doStuff();
}
Numbers in LightScript cannot begin or end with a .:
.5unknown: Decimal numbers must be prefixed with a `0` in LightScript (eg; `0.1`). (1:0)
> 1 | .5
| ^5.unknown: Numbers with a decimal must end in a number (eg; `1.0`) in LightScript. (1:1)
> 1 | 5.
| ^Instead use the explicit decimal forms:
0.5
5.00.5;
5.0;
While invisible characters are legal in strings, the only ones allowed in code
are (ascii-32), \n and \r\n. Tabs, non-breaking spaces, and exotic unicode
such as \u8232 raise SyntaxErrors.
In LightScript, a { at the beginning of a line parses as the start of an object, not a block.
For example, the following code breaks in LightScript:
if (true)
{
// body goes here
let x = 3
}unknown: Unexpected token, expected , (4:6)
2 | {
3 | // body goes here
> 4 | let x = 3
| ^
5 | }You must instead use the following style when using curly braces:
if (true) {
let x = 3
}if (true) {
let x = 3;
}
In the rare case that you wish to use an anonymous block, such as
function foo() {
// some code up here
{
// code in an anonymous block here
let x = 'private!'
}
// more code down here
let x = 5
}unknown: Unexpected token, expected , (5:8)
3 | {
4 | // code in an anonymous block here
> 5 | let x = 'private!'
| ^
6 | }
7 | // more code down here
8 | let x = 5you may prefix the anonymous block with a semicolon, as so:
function foo() {
// some code up here
;{
// code in an anonymous block here
let x = 'private!'
}
// more code down here
let x = 5
}function foo() {
// some code up here
{
// code in an anonymous block here
let x = "private!";
}
// more code down here
let x = 5;
return x;
}
Similarly, if using blocks with switch/case, you cannot write
switch (foo) {
case bar:
{
// contents of block here
let x = 3
}
}unknown: Unexpected token, expected , (5:10)
3 | {
4 | // contents of block here
> 5 | let x = 3
| ^
6 | }
7 | }and must instead write
switch (foo) {
case bar: {
// contents of block here
let x = 3
}
}switch (foo) {
case bar: {
// contents of block here
let x = 3;
}
}