At the moment, the intent for this project is to add a proper scripting language to [Bim](https://github.com/klange/bim), to which both configuration scripts and syntax highlighting will be ported.
Kuroko, [as its name should imply](https://toarumajutsunoindex.fandom.com/wiki/Shirai_Kuroko), will also be made available in [ToaruOS](https://github.com/klange/toaruos) as a general-purpose user language, and some utilities may end up being written in it.
Kuroko inherits some core features by virtue of following _Crafting Interpreters_, including its basic type system, classes/methods/functions, and the design of its compiler and bytecode VM.
C module sources are found in `src/` and provide optional added functionality. Each module source file corresponds to a resulting shared object of the same name that will be built to the `modules/` directory, which itself also contains modules written in Kuroko.
The standard set of C modules can be bundled into the interpreter, whether building statically or normally:
make clean; make KRK_ENABLE_BUNDLE=1
Additional options include `KRK_DISABLE_RLINE=1` to not link with the included rich line editing library (will lose tab completion and syntax highlighting in the repl) and `KRK_DISABLE_DEBUG=1` to disable debugging features (which has not been demonstrated to provide any meaningful performance improvement when the VM is built with optimizations enabled).
Experimental support is available for building Kuroko to run on Windows using MingW:
CC=x86_64-w64-mingw32-gcc make
A capable terminal, such as Windows Terminal, is required to run the interpreter's REPL correctly. The older "Command Prompt" (cmd.exe) is specifically known to _not_ work.
The string printed after all arguments have been printed can be changed with `end=`:
```py
print("Hello",end=" ")
print("World")
# → Hello World
```
_**Note:** When using the REPL with the rich line editor enabled, the line editor will automatically add a line feed when displaying the prompt if the previous output did not include one, and will display a gray left-facing triangle to indicate this._
Much like in Python 3, strings in Kuroko represent sequences of non-normalized Unicode codepoints. Both source files and the terminal in which Kuroko is running are expected to be UTF-8.
_**Implementation Note:** Generally, the internal representation of strings is their UTF-8 encoded form. When a subscript or slicing operation happens in which a codepoint index needs to be converted to an offset in the string, the most appropriate 'canonical' format will be generated and remain with the interned string until it is garbage collected. For strings containing only ASCII characters, no conversion is done and no additional copy is created. For all other strings, the smallest possible size for representing the largest codepoint is used, among the options of 1, 2, or 4. This approach is similar to CPython post-3.9._
Bytes objects can also be written as literals in the same format. Note that strings and bytes are not generally compatible with each other, so comparisons, concatenation, and so on will typically fail or raise exceptions.
Kuroko strings are sequences of Unicode codepoints without normalization applied. This means that while é and é may look the same in your terminal or text editor, they are different strings in Kuroko - the latter of the two is constructed from the codepoint for the letter "e" and the combining character for the acute accent.
```py
print('é'.encode())
# → b'\xc3/\xa9'
print('é'.encode())
# → b'e\xcc\x81'
```
Additionally, it means that the latter is _two_ codepoints and thus also a string of length 2.
Unicode normalization and combining characters are complicated topics requiring library support and Kuroko does not bake them into the language semantics.
_**Note:** A module to provide access to information from ICU, including string normalization and character types, is planned._
In a departure from Python, Kuroko has explicit variable declaration and traditional scoping rules. Variables are declared with the `let` keyword and take the value `None` if not defined at declaration time:
_**Note:** Identifier names, including for variables, functions, and classes, can be Unicode sequences. All non-ASCII codepoints are accepted as identifier characters._
After a variable is declared, assignments to it are valid as both expressions and statements. Kuroko provides assignment shortcuts like `+=` and `-=` as well as C-style postfix increment (`++`) and decrement (`--`).
```py
let x = 1
print(x++)
# → 2
print(x -= 7)
# → -5
print((x = 42))
# → 42
```
_**Note:** `var=` is used for keyword arguments, so be careful if you are trying to pass an assignment as a value to a function - wrap it in parentheses first or you may not get what you wanted._
If a default argument value is not provided, the expression assigned to it will be evaluated as if it were at the top of the body of the function. Note that this behavior intentionally differs from Python, where default values are calculated once when the function is defined; assigning a mutable object, such as a list, as a default value will create a new list with each invocation of the function in Kuroko, rather than re-using the same list. If you would like to have behavior like in Python, define the value outside of the function:
Blocks, including function `def` blocks and control flow structures like `if` and `for`, must be indented with spaces to a level greater than the enclosing block.
It is recommended that you use an editor which provides a clear visual distinction between tabs and spaces, such as [Bim](https://github.com/klange/bim).
When a function references a local from another function in which its definition is nested (or variables declared within a block), the referenced variables will continue to "live" in the heap beyond the execution of their original scope context.
If we define a function which declares a local variable and then define an inner function which references that variable, such as in the example below, each call to the other function will create a new instance of the variable and a new instance of the inner function. When the inner function is returned, it will take with it the variable it captured from the outer function and further calls to this instance of the inner function will use that variable.
Objects and classes in Kuroko work a lot like Python or similar languages in that they have an arbitrary and mutable set of fields, which may be methods or other values.
To create a basic object without methods, the `object` class is provided:
The `self` keyword is implicit in all methods and does not need to be supplied in the argument list. You may optionally include it in the method declaration anyway, for compatibility with Python:
Classes can also define fields, which can be accessed from the class or through an instance.
```py
class Foo():
bar = "baz"
def printBar(self):
print(self.bar)
let o = Foo()
o.printBar()
# → baz
print(Foo.bar)
# → baz
```
_**Note:** Instances receive a copy of their class's fields upon creation. If a class field is mutable, the instance's copy will refer to the same underlying object. Assignments to the instance's copy of a field will refer to a new object. If a new field is added to a class after instances have been created, the existing instances will not be able to reference the new field._
_**Note**: As in Python, all values are objects, but internally within the Kuroko VM not all values are **instances**. The difference is not very relevant to user code, but if you are embedding Kuroko it is important to understand._
Runtime exceptions are passed to the `except` block as a special variable `exception`. Exceptions from the VM are instances of built-in error classes with an attribute `arg`:
Exceptions can be generated with the `raise` statement. When raising an exception, the value can be anything, but subclasses of `__builtins__.Exception` are recommended.
`try`/`except` blocks can also be nested within each other. The deepest `try` block will be used to handle an exception. If its `except` block calls `raise`, the exception will filter up to the next `try` block. Either the original exception or a new exception can be raised.
Modules are run once and then cached, so if they preform actions like printing or complex computation this will happen once when first imported. The globals table from the module is the fields table of an object. Further imports of the same module will return the same object.
When importing a module, the names of members which should be imported can be specified and can be renamed:
```py
from demomodule import foo
print(foo)
# → bar
```
```py
from demomodule import foo as imported
print(imported)
# → bar
```
_**Note:** When individual names are imported from a module, they refer to the same object, but if new assignments are made to the name it will not affect the original module. If you need to replace values defined in a module, always be sure to refer to it by its full name._
Modules can also come in the form of _packages_. Packages are modules that contain other modules. To make a package, create a directory in one of the module import paths with the name of your package and place a file named `__init__.krk` in that directory. This file will be run when the package is imported, but if you only want to use packages for namespacing it does not need to have any content.
Say we have a directory tree as follows:
```
modules/
foo/
__init__.krk
bar/
__init__.krk
baz.krk
```
With this directory tree, we can `import foo`, `import foo.bar`, or `import foo.bar.baz`.
When a module within a package is imported directly, as in `import foo.bar.baz`, its parent packages are imported in order and the interpreter ensures each has an attribute pointing to the next child. After the `import` statement, the top-level package will be bound in the current scope:
```py
import foo.bar.baz
print(foo)
print(foo.bar)
print(foo.bar.baz)
# → <module 'foo' from './modules/foo/__init__.krk'>
# <module 'foo.bar' from './modules/foo/bar/__init__.krk'>
# <module 'foo.bar.baz' from './modules/foo/bar/baz.krk'>
```
If we want to get at the module `baz` we can use `import ... as ...` to bind it to a name instead:
```py
import foo.bar.baz as baz
print(baz)
try:
print(foo) # NameError
except:
print(repr(exception))
# → <module 'foo.bar.baz' from './modules/foo/bar/baz.krk'>
# NameError: Undefined variable 'foo'.
```
Note that only module names can be specified as the first argument to `import` or `from`, and that if a module within a package has never been imported it will not be available from its package.
If we define something in `modules/foo/bar/baz.krk` we can access it either by its full name or through a `from` import:
```py
# modules/foo/bar/baz.krk
let qux = "hello, world"
```
```py
import foo.bar.baz
print(foo.bar.baz.qux)
from foo.bar.baz import qux
print(qux)
# → hello, world
# hello, world
```
When using `from ... import`, the imported name can be a module, package, or regular member of the module before the `import`. Multiple names can be imported at once, but only one level can be imported:
An exception will be raised if a tuple returned by the iterator has the wrong size for unpacking (`ValueError`), or if a value returned is not a tuple (`TypeError`).
The special method `__iter__` should return an iterator. An iterator should be a function which increments an internal state and returns the next value. If there are no values remaining, return the iterator object itself.
_**Note:** The implementation of iterators in Kuroko differs from Python. In Python, iterator objects have a `__next__` method while in Kuroko they are called as functions or using the `__call__` method on instances, which allows iterators to be implemented as simple closures. In Python, completed iterators raise an exception called `StopIteration` while Kuroko iterators are expected to return themselves when they are done._
As in the _Loops_ section above, an iterator may return a series of tuples which may be unpacked in a loop. Tuple unpacking is optional; if the loop uses a single variable, the tuple will be assigned to it with each iteration.
The `__repr__` method serves a similar purpose and is used when the REPL displays values or when they are used in string representations of collections. The implementations of `__str__` and `__repr__` can be different:
_**Note:** As all classes eventually inherit from `object` and `object` implements both `__str__` and `__repr__`, these methods should always be available._
Decorators are _expressions_, just like in Python, so to make a decorator with arguments create a function that takes those arguments and returns a decorator:
This will be slower in execution than a normal function call, as the interpreter will need to figure out where to place arguments in the requested function by examining it at runtime, but it allows for functions to take many default arguments without forcing the caller to specify the values for everything leading up to one they want to specifically set.
When used as parameters in a function signature, `*` and `**` before an identifier indicate that the function will accept arbitrary additional positional arguments and keyword arguments respectively. These options are typically applied to variables named `args` and `kwargs`, and they must appear last (and in this order) in the function signature if used.
The variable marked with `*` will be provided as an ordered `list`, and `**` will be an unordered `dict` of keyword names to values.
_**Note:** `*args` and `**kwargs` are especially useful in combination with Argument Expension (described below) to create decorators which do not need to know about the signatures of functions they wrap._
If an expanded list provides too many, or too few, arguments, an ArgumentError will be raised.
If an expanded dict provides parameters which are not requested, an ArgumentError will be raised.
If an expanded dict provides an argument which has already been defined, either as a positional argument or through a named parameter, an error will be raised.
The value of `expr` must be an object with an `__enter__` and `__exit__` method, such as a `fileio.File`. The `__enter__` method will be called upon entry and the `__exit__` method will be called upon exit from the block.
The result of `expr` can also be assigned a name for use within the block. Note that as with other control flow structures in Kuroko, this name is only valid within the block and can not be referenced outside of it, and the same is true of any names defined within the block. If you need to output values from within the block, such as in the typical case of opening a file and loading its contents, be sure to declare any necessary variables before the `with` statement:
```py
from fileio import open
let lines
with open('README.md') as f:
lines = [l.strip() for l in f.readlines()]
print(lines)
# → ["![logo]...
```
Note that you can declare a variable for the object used with `__enter__` and `__exit__` before the `with` statement:
If an early return is encountered inside of a `with` block, the `__exit__` method for the context manager will be called before the function returns.
```py
class ContextManager:
def __init__(title):
self.title = title
def __enter__():
print("Enter context manager", self.title)
def __exit__():
print("Exit context manager", self.title)
def doesANestedThing():
with ContextManager('outer'):
with ContextManager('inner'):
with ContextManager('triple'):
return 42
print('Should not print')
print('Should not print')
print('Should not print')
print(doesANestedThing())
# → Enter context manager outer
# Enter context manager inner
# Enter context manager triple
# Exit context manager triple
# Exit context manager inner
# Exit context manager outer
# 42
```
_**Note:** The implementation of `with` blocks is incomplete; exceptions raised from within a `with` that are not caught within the block will cause `__exit__` to not be called._
The compiler implements special handling when the decorators `@staticmethod` and `@property` are used with methods of a class.
`@staticmethod` will mark the decorated method as a regular function - it will not receive an implicit "self" and it will be attached to the `fields` table of the class and instances of that class, rather than the `methods` table.
`@property` will mark the decorated method as a property object. When it is retrieved or assigned to from the class or instance through a dot-accessor (eg. `foo.bar`), the wrapped method will be called intead.
Properties work differently from in Python:
```py
class Foo():
def __init__(self):
self._bar = 0
@property
def bar(self, *setter):
if setter:
print("Setting bar:", setter[0])
self._bar = setter[0]
else:
print("Getting bar.")
return self._bar
let f = Foo()
print(f.bar)
f.bar = 42
print(f.bar)
# → Getting bar.
# 0
# Setting bar: 42
# Getting bar.
# 42
```
_**Note:** Special handling when using `del` on a property is not yet implemented._
Kuroko's repl provides an interactive environment for executing code and seeing results.
When entering code at the repl, lines ending with colons (`:`) are treated specially - the repl will continue to accept input and automatically insert indentation on a new line. Please note that the repl's understanding of colons is naive: Whitespace or comments after a colon which would normally be accepted by Kuroko's parser will not be understood by the repl - if you want to place a comment after the start of a block statement, be sure that it ends in a colon so you can continue to enter statements.
Pressing backspace when the cursor is preceded by whitespace will delete up to the last column divisible by 4, which should generally delete one level of indentation automatically.
The tab key will also produce spaces when the cursor is at the beginning of the line or preceded entirely with white space.
When a blank line or a line consisting entirely of whitespace is entered, the repl will process the full input.
Code executed in the repl runs in a global scope and reused variable names will overwrite previous definitions, allowing function and class names to be reused.
You may be looking at the code examples and thinking Kuroko looks a _lot_ more like Python than "syntax similar to Python" suggests. Still, there are some differences, and they come in two forms: Intentional differences and unintentional differences.
Unintentional differences likely represent incomplete features. Intentional differences are design decisions specifically meant to differentiate Kuroko from Python and usually are an attempt to improve upon or "fix" perceived mistakes.
Two notable intentional differences thus far are:
- Kuroko's variable scoping requires explicit declarations. This was done because Python's function-level scoping, and particularly how it interacts with globals, is often a thorn in the side of beginner and seasoned programmers alike. It's not so much seen as a mistake as it is something we don't wish to replicate.
- Default arguments to functions are evaluated at call time, not at definition time. How many times have you accidentally assigned an empty list as a default argument, only to be burned by its mutated descendent appearing in further calls? Kuroko doesn't do that - it works more like Ruby.
There are two ways to connect Kuroko with C code: embedding and modules.
Embedding involves including the interpreter library and initializing and managing the VM state yourself.
C modules allow C code to provide functions through imported modules.
If you want to provide C functionality for Kuroko, build a module. If you want to provide Kuroko as a scripting language in a C project, embed the interpreter.
With either approach, the API provided by Kuroko is the same beyond initialization.
Kuroko is built as a shared library, `libkuroko.so`, which can be linked against. `libkuroko.so` generally depends on the system dynamic linker, which may involve an additional library (eg. `-ldl`).
If `newModuleScope` is non-zero, the interpreter will parse code in the context of a new _module_ and the `KrkValue` returned will be a `module` object.
If `newModuleScope` is zero, the return value will be the last value popped from the stack during execution of `sourceText`. This can be used, as in the REPL, when providing interactive sessions.
The arguments `fromName` provide the name of the module created by when `newModuleScope` is non-zero, and `fromFile` will be used when displaying tracebacks.
### Building Modules
Modules are shared objects with at least one exported symbol: `krk_module_onload_{NAME}` (where `{NAME}` is the name of your module, which should also be the name of your shared object file excluding the `.so` suffix).
Your module's `krk_module_onload_...` function should return a `KrkValue` representing a `KrkInstance` of the `vm.moduleClass` class.
If `hasKw` is non-zero, then the value in `argv[argc]` will represent a dictionary of keyword and value pairs. Positional arguments will be provided in order in the other indexes of `argv`.
Binding to `vm.builtins->fields` will make your function accessible from any scope (if its name is not shadowed by a module global or function local) and is discouraged for modules but recommended for embedded applications.
### Kuroko's Object Model
For both embedding and C modules, you will likely want to create and attach functions, classes, objects, and so on.
It is recommended you read [_Crafting Interpreters_](https://craftinginterpreters.com/contents.html), particularly the third section describing the implementation of `clox`, as a primer on the basic mechanisms of the _value_ system that Kuroko is built upon.
Essentially, everything accessible to the VM is represented as a `KrkValue`, which this documentation will refer to simply as a _value_ from here on out.
Values are small, fixed-sized items and are generally considered immutable. Simple types, such as integers, booleans, and `None`, are directly represented as values and do not exist in any other form.
More complex types are represented by subtypes of `KrkObj` known as _objects_, and values that represent them contain pointers to these `KrkObj`s. The `KrkObj`s themselves live on the heap and are managed by the garbage collector.
Strings, functions, closures, classes, instances, and tuples are all basic objects and carry additional data in their heap representations.
_Strings_ (`KrkString`) are immutable and de-duplicated - any two strings with the same text have the same _object_. (See _Crafting Interpreters_, chapter 19) Strings play a heavy role in the object model, providing the basic type for indexing into attribute tables in classes and instances.
_Functions_ (`KrkFunction`) represent bytecode, argument lists, default values, local names, and constants - the underlying elements of execution for a function. Generally, functions are not relevant to either embedding or C modules and are an internal implementation detail of the VM.
_Closures_ (`KrkClosure`) represent the callable objects for functions defined in user code. When embedding or building a C module, you may need to deal with closures for Kuroko code passed to your C code.
_Bound methods_ (`KrkBoundMethod`) connect methods with the "self" object they belong to, allowing a single value to be passed on the stack and through fields.
_Classes_ (`KrkClass`) represent collections of functions. In Kuroko, all object and value types have a corresponding `KrkClass`.
_Instances_ (`KrkInstance`) represent _user objects_ and store _fields_ in a hashmap and also point to the class they are an instance _of_. Instances can represent many things, including collections like lists and dictionaries, modules, and so on.
_Tuples_ (`KrkTuple`) represent simple fixed-sized lists and are intended to be immutable.
Finally, _native functions_ (`KrkNative`) represent callable references to C code.
Most extensions to Kuroko, both in the form of embedded applications and C modules, will primarily deal with classes, instances, strings, and native functions.
Two of the high-level collection types, lists and dictionaries, are instances of classes provided by the `__builtins__` module. While they are not their own type of `KrkObj`, some macros are provided to deal with them.
### Creating Objects
Now that we've gotten the introduction out of the way, we can get to actually creating and using these things.
The C module example above demonstrates the process of creating an object in the form of an instance of the `vm.moduleClass` class. All C modules should create one of these instances to expose other data to user code that imports them.
Most extensions will also want to provide their own types through classes, as well as create instances of those classes.
_**NOTE:** When creating and attaching objects, pay careful attention to the order in which you allocate new objects, including strings. If two allocations happen in sequence without the first allocated object being stored in a location reachable from the interpreter roots, the second allocation may trigger the garbage collector which will immediately free the first object. If you need to deal with complex allocation patterns, place values temporarily on the stack to prevent them from being collected._
```c
/* Create a class 'className_' and attach it to our module. */
Here we have created a new class named `MyNameClass` and exposed it through the `fields` table of our module object under the same name. We're not done preparing our class, though:
We also want to make sure that our new class fits into the general inheritance hierarchy, which typically means inheriting from `vm.objectClass` - we do this by passing `vm.objectClass` to `krk_newClass` as a base class.
When attaching methods, notice the `.` at the start of the name. This indicates to `krk_defineNative` that this method will take a "self" value as its first argument. This affects how the VM modifies the stack when calling native code and allows native functions to integrate with user code functions and methods.
In addition to methods, native functions may also provide classes with _dynamic fields_. A dynamic field works much like a method, but it is called implicitly when the field is accessed. Dynamic fields are used by the native classes to provide non-instance values with field values.
If your new instances of your class will be created by user code, you can provide an `__init__` method, or any of the other special methods described in the Examples above.
When you've finished attaching all of the relevant methods to your class, be sure to call `krk_finalizeClass`, which creates shortcuts within your class's `struct` representation that allow the VM to find special functions quickly:
Specifically, this will search through the class's method table to find implementations for functions like `__repr__` and `__init__`. This step is required for these functions to work as expected as the VM will not look them up by name.
There are two ways to attach internal state to new types:
- If state lookup does not need to be fast and consists entirely of values that can be represented with Kuroko's type system, use the instance's `fields` table.
- If state lookup needs to be immediate and involves non-Kuroko types, extend `KrkInstance`.
The first approach is easy to implement: Just attach named values to an instance's `fields` table where appropriate, such as in the type's `__init__` method.
The second approach requires some additional work: The class must specify its allocation size, define a structure with a `KrkInstance` as its first member (followed by the additional members for the type), ensure that values are properly initialized on creation, and also provide callbacks for any work that needs to be done when the object is scanned or sweeped by the garbage collector.
The `range` class is an example of a simple type that extends `KrkInstance`:
```c
struct Range {
KrkInstance inst;
krk_integer_type min;
krk_integer_type max;
};
```
As the additional members `min` and `max` do not need any cleanup work, the `range` class only needs to indicate its allocation size when it is defined:
The `list` class, however, stores Kuroko objects in a flexible array:
```c
typedef struct {
KrkInstance inst;
KrkValueArray values;
} KrkList;
```
And must bind callbacks to ensure its contents are not garbage collected, and that when the list itself is garbage collected the additional memory of its flexible array is correctly freed: