-
Notifications
You must be signed in to change notification settings - Fork 0
Documentation
kesh is what JavaScript (Mocha) might have looked like if it was designed today, without its historical baggage.
[features]: import 'javascript'
[type-system]: import 'typescript'
extensions: import './extensions.kesh'
good-parts: features.filter (part: #feature) -> part.good = true
kesh: essence(good-parts) + essence(type-system) + extensions
Note: This is a work in progress. Nothing is written in stone.
Values are immutable by default.
Everything is an expression, except for declarations.
Values may be named with :
.
answer: 42
Here, the name answer
is defined as the value 42
, a constant rather than a variable (single assignment).
Naming is not an expression but a declaration. Names are declared within blocks, tuples and collections.
It is possible to redefine a name, but this is opt-in and explicit.
A block is a sequence of declarations and expressions, in that order, with lexical scoping and shadowing of names.
It is itself an expression that evaluates to the value of its last evaluated expression.
{
a: 20
b: 22
a + b
}
--> #number 42
For example, a block can be used to produce a value inside a temporary local scope.
a: 1
answer: { a: 20, b: 22, a + b }
a --> #number 1
b --> b is not defined (in this scope)
answer --> #number 42
The outermost scope is always a block. The example above is a block that evaluates to 42
.
Blocks are used for text interpolation, computed keys, field access, functions, modules and wherever compound declarations or inline expressions are needed.
Blocks can be written without brackets, using significant indentation instead.
answer:
a: 20
b: 22
a + b
answer --> #number 42
There are four primitive data types:
Truth and number values are as described in na, except that numbers are IEEE 754 double-precision 64-bit floating-point values due to the underlying ECMAScript runtime.
Primitive data types have value semantics.
Single-quoted text literals produce verbatim text values.
Double-quoted text literals support escape sequences and interpolation.
single: 'a "verbatim" text'
double: "an \"escaped\" text"
Interpolation is simply inline expressions (blocks) evaluated within a double-quoted text literal.
answer: "The answer is { 20 + 22 }" --> #text "The answer is 42"
greeting: "Hey, { name }!" --> #text "Hey, Joe!"
Text literals may be tagged with a function.
For example, the function raw
may be used to create a text with interpolation but without escape sequences.
path: raw"C:\Users\{ username }\Documents"
See also embedded languages and regular expressions.
Multiline single- and double-quoted text literals follow the same rules as Julia's triple-quoted string literals.
hello: '''Hello,
world!'''
--> #text "Hello,\nworld!"
Symbols are human-readable values that are guaranteed to be unique. Symbols have a leading $
sigil (the $ stands for "symbol") and are either global or secret.
The naming rules of global symbols are the same as for names. Secret symbols may be given a text description.
global: $cool
secret: $('unique, non-global symbol') -- can only be referenced by the name it is assigned to
Symbols may be used as hidden fields of collections.
There are two fundamental composite data types:
Tuple is the most basic data structure.
A tuple of one value evaluates to that value. An empty tuple evaluates to the unit type #none
.
something: (42) --> #number 42
nothing: () --> #none
A tuple of 2+ values is the grouping of those values.
lat: 51.9972
lon: -0.7419
coordinates: (lat, lon) --> #tuple (51.9972, -0.7419)
A tuple is 0-indexed. Its values are accessed using dot notation.
coordinates.0 --> #number 51.9972
coordinates.1 --> #number -0.7419
Tuples are used for the basic grouping of values, nesting of expressions, function parameter definitions and arguments, multiple return values, the unit type and more.
Due to the underlying ECMAScript runtime, tuples are reference types. Ideally, they would have value semantics.
Collections represent various data structures with keyed/indexed values.
The [ ]
literal creates a plain collection of values. Each value is either keyed or implicitly 0-indexed.
keyed: [answer: 42] --> #collection [ answer: 42 ]
indexed: [1, 2, 3] --> #collection [ 1, 2, 3 ]
mixed: [1, 2, 3, length: 3] --> #collection [ 1, 2, 3, length: 3 ]
Its keys may be names, whole #number
s, #text
s or #symbol
s. Its values may be of any type.
If no key is specified and the value is provided by an identifier, that identifier's name will be used as the value's key.
answer: 42
keyed: [answer] --> #collection [ answer: 42 ]
If the identifier is provided as a tuple, it will evaluate to its value.
indexed: [(answer)] --> #collection [ 42 ]
Due to the underlying ECMAScript runtime, name and number keys are evaluated and stored in memory as #text
.
[ ]
can also represent more advanced data structures, such as ECMAScript's object, array, set, map and more.
Objects are equivalent to ECMAScript's Object.
ada: object[name: 'Ada', age: 27]
joe: object[name: 'Joe', age: 27]
Objects inherit all its familiar prototype methods.
ada.to-string()
--> #text "[object Object]"
As in ECMAScript, primitive values are automatically boxed as objects when needed, and even functions are (also) objects.
Arrays are equivalent to ECMAScript's Array.
As in ECMAScript, an array is an object whose values are implicitly assigned 0-indexed numeric keys.
people: array[ada, joe]
-- > #array [ #object [ … ], #object [ … ] ]
Unlike plain collections and objects, values provided by identifiers will not create keys of those names.
Arrays inherit all the familiar prototype methods from ECMAScript's Array
.
people.index-of ada
--> #number 0
All of ECMAScript's collection types are supported – indexed and keyed.
players: set[]
players.add ada
players.add joe
score: map[
(ada, 22)
(joe, 20) -- (key, value) where the key can be of any type
]
As a prototype-based language, inheritance is achieved with delegation (single, differential inheritance).
Delegation is done by applying an existing collection to a collection literal, just like function application.
primate: [
tail: true
]
--> #collection [ tail: true ]
human: primate [ -- delegation
tail: false -- overridden value
walks: true
talks: true
]
--> #collection [ tail: false, talks: true, walks: true ]
joe: human [
name: 'Joe'
]
--> #collection [ name: "Joe", tail: false, talks: true, walks: true ]
Conceptually, a collection is (also) a function taking a collection, returning a copy of it with itself applied as prototype.
An alternative is to use composition within a factory function.
This technique doesn't use prototypal inheritance, and allows fine grained control over private and public fields.
new-person: (options) ->
[name]: options -- private field (unpacked from options)
human: new-human options -- private field (composition)
say: (words) ->
human.speak("{ name } says: { words }") -- no "this" needed
[name, say] -- public fields returned from function
joe: new-person [
name: 'Joe'
]
joe
--> #collection [ name: "Joe", say: (words) -> { … } ]
joe.say("Hi!")
--> #text "Joe says: Hi!"
Another alternative is concatenation of collections.
This is achieved by using the spread operator to copy all enumerable fields from an existing collection.
joe: [
primate...
human...
name: 'Joe'
]
Similarly, updated versions of a collection may be created by concatenating values onto the existing collection.
joe-updated: [joe..., name: 'Joey']
people-updated: array[people..., jane]
Using a symbol as a field's key will make the field non-enumerable.
person: object[
name: 'Joe'
$cool: true
]
--> #object [ name: "Joe", $cool: true ]
keys person
--> #array [ "name" ]
It is still accessible by dot notation.
person.$cool
--> #truth true
Collection keys can be specified as an expression by using a 1-tuple (or inline block).
This allows secret symbols to be used as keys. To access the field, the identifier holding the symbol must be in scope.
secret: $('hidden talent') -- secret symbol
person: object[
name: 'Joe'
(secret): 'I can moonwalk!'
]
A tuple or a collection's values may be accessed using the .
operator.
people.0 -- number (index)
--> #object [ name: "Joe", age: 27 ]
person.name -- name
--> #text "Joe"
person.$cool -- global symbol
--> #truth true
Fields may also be accessed similar to function application. After all, field access is essentially a getter function.
person.('full name') -- text
--> #text "Joe Doe"
person.(secret) -- secret symbol
--> #text "I can moonwalk!"
The bottom prototype is the unit type #none
, a collection that only ever returns itself.
Accessing a missing field will therefore not produce an error, but always return #none
.
person.foo.bar
--> #none
Tuples and collections may be unpacked, with support for the rest operator.
Tuples are unpacked by index.
(name, age): ('Joe', 27)
Objects are typically unpacked by keys.
[name, age, ...rest]: person
In fact, any collection may be unpacked by keys using [ ]
.
[length]: array[joe, ada]
length --> #number 2
Similarly, any iterable collection may be unpacked by iteration using < >
.
<first, ...rest>: 'Hey'
Arrays are typically unpacked by iteration (that is, by index).
<head, ...tail>: [1, 2, 3]
Collection keys may be renamed with the as
rename operator.
['full name' as name]: person
A default value may be provided with the ?
existential operator.
[age ? 0]: person
Functions are first-class citizens with closure.
All functions have an arity of 1. That single parameter/argument can of course be a tuple of zero or multiple values.
sum: (a, b) -> a + b
The definition of a function's parameter(s) is the unpacking of a tuple or a collection (arguments).
The function's body is an expression, either inline or a block.
greet: [name, age] ->
name: name if age > 12 else 'kid'
"Hey, { name }!"
Functions are equivalent to ECMAScript's arrow functions. See also methods.
A function is applied to its argument, which can be a tuple of values.
sum(20, 22) -- equivalent to: sum (20, 22)
--> #number 42
The argument could also be a previously declared tuple.
numbers: (20, 22)
sum numbers
--> #number 42
Because a 1-tuple is equivalent to its value, a function may be applied to a single value without the use of parens.
joe: [name: 'Joe', age: 27]
greet joe -- equivalent to: greet(joe)
--> #text "Hey, Joe!"
Function application is right associative.
print greet joe -- equivalent to: print(greet(joe))
Functions are not hoisted. A function cannot be used before it is defined, logically enough.
Because a function's arguments are unpacked, it can't receive a tuple as a tuple.
To receive its values as one argument, a workaround is to first convert the tuple to an array.
print (1, 2, 3) --> #text "1 2 3"
print #array (1, 2, 3) --> #array [1, 2, 3]
this
is an intricate feature of ECMAScript methods that often leads to binding issues. In kesh, this
is not a problem.
A method is a field that is a function that is bound to its collection. Within this function, @
references the bound collection and @foo
references the field named foo
of the bound collection (equivalent to this
and this.foo
in ECMASCript).
byron: [
name: 'Lord Byron'
speak(): "Hi, my name is { @name }!"
]
Unlike ECMAScript, if a method is later detached from its collection, it will still be bound to it.
speak: byron.speak -- speak is bound to byron
speak()
--> #text "Hi, my name is Lord Byron!"
A method inherited through prototypal delegation is bound to the new collection.
ada: byron [name: 'Ada'] -- ada inherits speak from byron
speak: ada.speak -- speak is bound to ada, not byron
speak()
--> #text "Hi, my name is Ada!"
Methods are also collections having apply
, call
and bind
methods.
byron.speak.apply [name: 'Ada']
--> #text "Hi, my name is Ada!"
All code is evaluated in strict mode, so @
should never evaluate to global
.
A function may return multiple values by simply returning a tuple.
sum-diff: (a, b) -> (a + b, a - b)
(sum, diff): sum-diff(100, 42)
sum --> #number 142
diff --> #number 58
Variadic functions are possible with the rest operator.
sum: (...numbers) ->
numbers.reduce (acc, num) -> acc + num
sum(1, 2, 3, 4)
--> #number 10
Due to the underlying ECMAScript runtime, the rest operator returns an array, not a tuple. Which can be handy.
Functions that perform side effects are defined with \->
.
big-red-button: () \-> missile.launch()
A side-effecting function will return #none
instead of implicitly returning its last evaluated expression.
The return
statement from the return
extension may however be used to return a value.
Like Python's decorators, only without special syntax. They're simply (factory) functions applied to values.
invincible: (collection) ->
collection [name: 'invincible'] -- delegation
favorite-color: (color) -> (collection) -> -- currying
[collection..., color] -- concatentation
black-knight: invincible favorite-color('blue') [
name: 'The Black Knight'
color: 'black'
speak(): "I'm { @name }!"
]
black-knight.color
--> #text "blue"
black-knight.speak()
--> #text "I'm invincible!"
Conditionals are either a traditional if…(then)…else…
construct or the more concise ternary …if…else…
operator.
then
is only required for the inline form of the traditional construct, which doesn't allow nesting, only else if
.
traditional: if age < 13
'kid'
else if age < 20
'teenager'
else
'adult'
inline: if age < 13 then 'kid' else if age < 20 then 'teenager' else 'adult'
ternary: 'kid' if age < 13 else 'teenager' if age < 20 else 'adult'
The condition must evaluate to a #truth
value, there's no implicit truthy/falsy.
The coercion and existential operators help with this.
Using :
for naming means that =
is free to be used as the equality operator, which makes sense.
=
represents strict equality, where both operands must be of the same type. /=
(≠
) represents strict inequality.
answer = 42 --> #truth true (strict equality)
answer ≠ '42' --> #truth true (strict inequality)
Similarly, ~=
represents loose equality, and ~/=
(~≠
) loose inequality, with automatic type coercion of operands.
answer ~= '42' --> #truth true (loose equality)
answer ~≠ '42' --> #truth false (loose inequality)
Logical operators use words.
not true
true and false
true or false
Operands must evaluate to a #truth
value, there's no implicit truthy/falsy.
The coercion and existential operators help with this.
The coercion operator ~
coerces its operand to a #truth
value.
It evaluates to true
if its operand is truthy and false
if it is falsy.
collection: []
print "It is { 'truthy' if ~collection else 'falsy' }"
--> #text "It is truthy"
The existential operator ?
can be used as a unary operator or as a binary operator.
As a trailing unary operator, it evaluates to false
if its operand is #none
and true
for any other value.
if page.title? and page.author?
-- page has title and author
As a binary operator, it functions as the nullish coalescing operator, evaluating to the right operand if the left is #none
.
title: page.title ? 'Default title'
This is logically equivalent to:
title: page.title if page.title? else 'Default title'
...
is used for both spread and rest operators. Unlike ECMAScript, the spread operator must appear after the identifier.
spread: [items..., item]
rest: (...arguments) -> print arguments
Due to the underlying ECMAScript runtime, the spread operator produces a shallow copy only.
The as
operator renames a key when unpacking.
[age as years-old]: ada
It is also used by some extensions, with similar semantics.
Arithmetic operators require that both operands are of the type #number
.
4 + '2' --> a type error, not "42"
4 + #number '2' --> #number 6
Text should be concatenated using interpolation.
In addition to the standard arithmetic operators +
, -
, /
and *
, kesh also has the following:
-
div
for integer division (floored division) -
mod
for modulo (floored division, same sign as divisor) -
rem
for remainder (truncated division, same sign as dividend) -
pow
for exponentiation
The range operator ..
creates an iterable range of numbers or characters, either finite (inclusive) or infinite.
finite-numbers: 1..7
finite-characters: 'a'..'z'
infinite-numbers: 1..
infinite-characters: 'À'..
When a finite range is wrapped in a collection literal, it creates a collection having those values.
numbers: [0..9]
characters: set['a'..'f']
inherits
tests whether collection A has collection B somewhere in its prototype chain.
joe inherits primate
--> #truth true
is-a
tests whether collection A's prototype is collection B.
joe is-a human
--> #truth true
is
and isnt
test whether a value is of a runtime type.
answer: 42
answer is #number --> #truth true
answer isnt #text --> #truth true
has
tests whether a collection has a value.
people has joe -- array
--> #truth true
players has ada -- set
--> #truth true
ada has 'name' -- object
--> #truth true
An alias for the active runtime's output function. Usually, this is console.log
.
A simple Hello World program is therefore:
print 'Hello, world!'
Returns the runtime type of the evaluated operand as #text
, equivalent to ECMAScript's typeof
operator.
type answer
--> #text "number"
When used in a type context, it returns the compile time type, equivalent to TypeScript's typeof
operator.
verify: (x) -> x = 42
#return: #ReturnType(type verify) -- #truth
Returns an array of a collection's own enumerable keys as #text
.
keys joe
--> #array [ 'name' ]
If the second argument is true
, it also returns any inherited enumerable keys.
keys(joe, true)
--> #array [ 'name', 'tail', 'talks', 'walks' ]
When used in a type context, it is equivalent to TypeScript's keyof
operator.
#person: [name: #text, age: #number]
#keys: keys #person -- "name" | "age"
Returns a collection's own enumerable values as an array.
values joe
--> #array [ "Joe" ]
If the second argument is true
, it also returns any inherited enumerable values.
values(joe, true)
--> #array [ "Joe", false, true, true ]
Returns a collection's own enumerable fields as an array of (key, value)
tuples.
fields joe
--> #array [ ("name", "Joe") ]
If the second argument is true
, it also returns any inherited enumerable fields.
fields(joe, true)
--> #array [ ("name", "Joe"), ("tail", false), ("talks", true), ("walks", true) ]
Creates a shallow copy of a collection.
joe2: copy joe
If the second argument is true
, it retains the prototype chain.
joe2: copy(joe, true)
joe2 is-a human
--> #truth true
Creates a structured clone of a collection (a deep copy).
joe2: clone joe
If the second argument is true
, it also copies the prototype chain.
joe2: clone(joe, true)
joe2 is-a human
--> #truth true
kesh builds on TypeScript's gradual, inferred, structural type system, with some differences.
kesh only has types, denoted by a leading #
.
Unlike TypeScript's interfaces, types are immutable and can not be extended.
#string: #text -- alias
#point: (#number, #number) -- tuple
#colorful: [color: #text] -- object
#circle: [radius: #number]
#colorful-circle: #colorful & #circle -- intersection
#texts: #text[] -- array, alternatively #array(#text)
#result: #text | #texts -- union
#greet: (#text) -> #text -- function
#players: #set(#player) -- set
#scores: #map(#player, #number) -- map
Collection types may be used as prototypes, as they are at the same time type definitions and actual collections for delegation.
Syntactically, delegation to a collection type is the same as conversion of runtime types.
Conceptually, a collection type is (also) a function taking a collection and applying itself as its prototype. Any literal fields of the collection type is thus inherited through prototypal delegation.
Fields may be marked as optional using the existential operator ?
, defaulting to #none
.
Alternatively, optional fields may use a trailing existential operator to provide a default value that's compatible with its type.
#primate: [ -- definition
tail: #truth -- type
name?: #text -- type (optional field, defaulting to #none)
]
#human: #primate [ -- delegation
tail: false -- literal
name: #text ? 'Anonymous' -- type (optional field with a default value)
]
joe: #human [ -- delegation
name: 'Joe' -- literal
]
joe.tail
--> #truth false
joe is-a #human
--> #truth true
joe inherits #primate
--> #truth true
An index signature is defined using a valid type as the key.
#texts-and-numbers: [
#text: #text | #number
length: #number
name: #text
]
The type of a name may be declared in advance.
name: #text
-- …
name: 'Ada'
As in TypeScript, the name cannot be referenced before it has been assigned a value.
A collection type can also be a protocol.
#walker-talker: [
walk: (#number) -> #text
talk: (#text) -> #text
]
Generics work just like in TypeScript.
#box<#type>: [
contents: #type
]
number: #box<#number>: [
contents: 42
]
text: #box<#text>: [
contents: 'hello'
]
#identity<#type>: (#type) -> #type
identity: <#type>(x: #type) -> #type { x }
number-id: #identity<#number>: identity
number-id 42 --> #number 42
#dynamic
#some
#none
#never
#dynamic
is equivalent to TypeScript's any
, disabling type checking. It is discouraged, though necessary in some situations.
#some
is the top type, equivalent to TypeScript's unknown
, which is a type-safe version of TypeScript's any
.
#none
is the unit type, conceptually a special collection that only ever returns itself.
#never
is the bottom type, equivalent to TypeScript's never
.
When used as a function, runtime data types perform type conversion.
truth: #truth 42 --> #truth true
number: #number '42' --> #number 42
text: #text 42 --> #text "42"
symbol: #symbol 42 --> #symbol $("42")
tuple: #tuple 42 --> #number 42
array: #array 42 --> #array [ 42 ]
A file is a module is a block.
Because it is a block, the last evaluated expression of a file is the module's exported value.
To prevent exporting of values by accident, the IDE should highlight a module's last expression.
By convention, the last line is an object of values to export.
-- file: deep-thought.kesh
answer: 42
respond: () -> print answer
now: temporal.now.plain-date-time-ISO()
duration: temporal.duration.from [years: 7500000] -- 7.5 million years
timeout: duration.total [unit: 'millisecond', relative-to: now]
ask: (question: #text) -> set-timeout(respond, timeout)
[ask] -- exported
Default exports are considered harmful, but can be named explicitly if needed.
[ask, default: answer]
The import
function statically imports another module's exported values.
merge: import 'lodash.merge'
Exported objects may be unpacked upon import.
-- file: asker.kesh
[ask]: import 'deep-thought.kesh'
ask 'What is the answer to the ultimate question of life, the universe, and everything?'
-- 7.5 million years later…
--> #number 42
Any default export must be explicitly (re)named upon import.
[default as lodash]: import 'lodash'
Compiled programming languages can be embedded within kesh using backticks.
Tagged code literals (combined with extensions) enable the embedding of other compiled programming languages.
Untagged code literals are evaluated as TypeScript/JavaScript.
```
let message: string = 'Hello, world!';
```
print message
See the regular expressions extension.
The language core is small by design. Any additional language features must be explicitly enabled using an interpreter directive at the top of the file, specifying both the language version and any features to be enabled or disabled.
kesh 2021 standard
Tying the source code to a specific version enables the language to evolve without risk of breaking backward compatibility. Legacy source code is guaranteed to always run as expected, using the specified version of the language.
The directive may also take the form of a shebang, using /usr/bin/env
to resolve the kesh
executable.
#!/usr/bin/env kesh 2021 standard
This serves two purposes, as the file can now be easily made executable on *nix systems.
Alternatively, a configuration file may specify the project-wide language version and features.
Some possible extensions are explored in Extensions.
List of reserved names in kesh:
Any keyword may be used as a name, also within blocks. The keyword will no longer be available within that scope.
The IDE or compiler may raise a warning when using a keyword as a name.
-- this is a line comment
IDE/compiler warnings may be silenced using a hush
comment.
noisy code -- hush
The debugger may be invoked with a debug
comment.
problematic code -- debug
This programming language only exists as a design. I'm unlikely to ever get the chance to write a compiler for it.
Feel free to steal any parts of it that you like. Ideas are better stolen than forgotten. (They're not my ideas anyway.)