KSL Syntax
Overview
The syntax of KSL is inspired by the Wolfram Language and LISP. It uses prefix notation exclusively, similar to LISP, and does not support infix operators by design.
All syntax in KSL is constructed from three primary components:
- Basic Elements
- Lists
- Function Calls
The following sections detail each of these constructs.
Basic Elements
KSL provides five fundamental data types: Symbols, Atoms, Strings, Numbers, and Objects.
Symbols
Symbols are identifiers used for variables and function names.
A symbol’s name must adhere to the following rules:
- Starting Character: A symbol must begin with a letter or an underscore (
_). It cannot begin with a number or a punctuation character. - Subsequent Characters: Subsequent characters can be any sequence of letters, numbers, underscores (
_), or single quotes ('). - Private Symbols: By convention, any symbol that begins with an underscore is considered private. Private symbols are local to their module and cannot be loaded by other files.
Examples of valid symbols include a, Add, _privateFunc', and _你好👋'.
Atoms
Atoms serve as unique, constant identifiers, similar to enums in other languages.
An atom’s name must adhere to the following rules:
- Prefix: An atom must begin with a hash symbol (
#). - Starting Character: The character immediately following the
#must be a letter. It cannot be a number, underscore, or punctuation character. - Subsequent Characters: All subsequent characters can be any sequence of letters, numbers, underscores (
_), or single quotes (').
For instance, #some'tag and #你好👋' are valid atoms.
The boolean values true and false are represented by the atoms #t and #f, respectively.
Atoms are commonly used to represent status codes, such as #ok and #err.
Strings
Strings are immutable sequences of characters enclosed in double quotes (").
They can span multiple lines and preserve all whitespace, including newlines and indentation.
KSL strings do not support standard escape sequences.
To embed special characters, use a hash symbol (#) followed by the character’s decimal Unicode code point.
Let[s, "Hello, world🌏,
Line breaks are directly supported.
Leading whitespace is also preserved."];
Print[s];
(* =>
Hello, world🌏,
Line breaks are directly supported.
Leading whitespace is also preserved.
*)
(* Insert a newline character (#10) *)
Let[s', Concat[s, #10, "This appears on a new line."]];
Print[s'];
(* =>
Hello, world🌏,
Line breaks are directly supported.
Leading whitespace is also preserved.
This appears on a new line.
*)
Numbers
All numeric literals in KSL are treated as 64-bit floating-point numbers (f64).
Numbers can be written in integer format or with a fractional part.
The integer part is mandatory; a leading decimal point is not a valid number.
Scientific E-notation is also supported, such as 65.43e-21.
Objects
Objects are collections of key-value pairs used to represent structured data. Each object is associated with a type tag that identifies its kind.
Creation and Initialization
The Object function creates a new object and requires, at a minimum, a type tag.
(* Create an empty object with the type tag #TypeName *)
Let[obj, Object[TypeName]];
An object’s type can be verified with the GetType function:
Print[Eq[GetType[obj], #TypeName]];
(* => #t *)
Objects can be initialized with a set of key-value pairs by providing a list of {key, value} pairs as the second argument to Object.
Let[obj2, Object[Another, {
{ #k1, "v1" },
{ #k2, 2 },
{ #key3, #value'3 }
}]];
Print[obj2];
(* => Object[Another]{k2, key3, k1} *)
Immutability and Updates
A core principle in KSL is immutability. Objects are not modified in-place; instead, functions that operate on them return a new, modified instance.
The Set function is used to add or update a property.
It returns a new object containing the change.
To persist this change, the variable must be rebound to the new instance using Let, effectively shadowing the original value.
(* Set returns a new object; Let rebinds obj to it *)
Let[obj, Set[obj, #key1, "val1"]];
Let[obj, Set[obj, #key2, #val2]];
This pattern is standard for most data-manipulation operations.
A notable exception is the built-in CloseStream function, which directly modifies a stream object to release underlying system resources.
Inspection
When printed, an object displays its type tag followed by its keys.
Print[obj];
(* => Object[TypeName]{key2, key1} *)
A list of all keys can be retrieved with the Keys function.
As objects are implemented internally with a hash map, the order of keys is not guaranteed.
Print[Keys[obj]];
(* => {#key2, #key1} *)
Property Access
The Get function retrieves the value associated with a given key.
Print[Get[obj, #key2]];
(* => #val2 *)
The Delete function returns a new object with the specified key removed.
The variable must be rebound to this new object to update its state.
Let[obj, Delete[obj, #key1]];
Print[obj];
(* => Object[TypeName]{key2} *)
Lists
Lists are ordered, heterogeneous collections of elements.
Syntax and Creation
A list is defined by enclosing comma-separated elements in curly braces ({}). Elements can be of any type.
{1, "2", #a3} (* A valid list *)
{1, 2, {3, {4}}} (* A nested list *)
Usage Conventions
By convention, functions that may result in an error return a list with a status atom, typically in the format {#err, "error message"}.
Function Calls
Basic Syntax
Function calls in KSL use prefix notation.
The function name is followed by square brackets ([]) that enclose a comma-separated list of arguments.
FunctionName[arg1, arg2]
A function with no arguments is called with empty brackets, as in Unit[].
Naming Conventions
All function names are symbols. By convention, built-in functions begin with an uppercase letter.
Anonymous Functions
Anonymous functions are constructed via the Fun function
and may be invoked directly or bound to a symbol prior to invocation,
which is also how we define functions.
(* Method 1: Invoke directly *)
Fun[{ n }, Add[n, 1]][2]; (* => 3 *)
(* Method 2: Bind to a symbol *)
Let[add1, Fun[{n}, Add[n, 1]]];
add1[3]; (* => 4 *)