Thinkmine.Kobo.Lang 0.1.12

dotnet tool install --global Thinkmine.Kobo.Lang --version 0.1.12
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Thinkmine.Kobo.Lang --version 0.1.12
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Thinkmine.Kobo.Lang&version=0.1.12
                    
nuke :add-package Thinkmine.Kobo.Lang --version 0.1.12
                    

Kobo command-line compiler

NuGet package: thinkmine.kobo.lang

Command: kobo

Kobo is a compact, modern language for writing .NET code. Its declarations, optional semicolons, delegate shorthand, and trailing lambdas are transformed into C#. The compiler then uses Roslyn to produce one .NET assembly from a single .kobo source file or the Kobo files in a folder.

This package is a preview. The NuGet package ID is separate from the command name: install thinkmine.kobo.lang, then invoke kobo.

Install and update

Install the .NET 11 SDK for .NET tool management and development. Running the compiler and generated applications requires a compatible .NET 11 runtime.

dotnet tool install --global thinkmine.kobo.lang --prerelease

To update an existing installation:

dotnet tool update --global thinkmine.kobo.lang --prerelease

For a repository-local tool installation:

dotnet new tool-manifest
dotnet tool install thinkmine.kobo.lang --prerelease
dotnet tool run kobo build hello.kobo

Create the tool manifest only if the repository does not already have one. Restore tools in an existing checkout with dotnet tool restore. These installation commands require the package to be available from a configured NuGet source; see Building from source for local packages.

Write, build, and run a program

For a single-file script, put #:script on the exact first line:

#:script
import * from System
Console.WriteLine("Hello from Kobo!")

Run it directly with the .NET 11 SDK:

kobo run hello.kobo

kobo run transpiles the file to temporary C# and executes it through dotnet run --file, using the SDK's C# file-based app support. It requires a single .kobo file with #:script as its exact first line (no preceding blank lines, comments, or whitespace). The old #:app marker is rejected. SDK, property, and package directives are preserved. Native AI scripts receive the compiler's runtime references and the adjacent kobo.ai.json configuration. AI scripts default to managed execution (PublishAot=false) to preserve the runtime's reflection-based JSON serialization. The application runs with the source directory as its working directory; standard input/output/error are inherited and its exit code is returned. Temporary generated files are removed after execution. Transpilation or build errors prevent execution and produce a nonzero exit code.

For a conventional program with an explicit entry point:

Save this as hello.kobo:

import * from System

module Examples/Hello

[public] class:Program
{
    [public, static] function:void Main(string[] args)
    {
        var message = "Hello from Kobo!"
        Console.WriteLine(message)
    }
}

Build and run:

kobo build hello.kobo --output-directory ./build --name Hello --publish-code
dotnet ./build/Hello.dll

The compiler writes Hello.dll and, for a console application, Hello.runtimeconfig.json. The optional --publish-code flag also writes Hello.cs, allowing you to inspect the generated C#.

Compiler options

kobo build [<input.kobo>|<folder>] [--recursive] [--output-directory <directory>] [--name <assembly-name>] [--publish-code]
Option Behavior
<input.kobo> or <folder> Builds one file or all Kobo files in a folder. With no input, builds the current directory. Quote paths containing spaces.
--recursive Includes subfolders in directory builds. Not valid for a file input.
--output-directory <directory> Writes output to this directory, creating it if needed. Defaults to the input file's directory or the folder being built.
--name <assembly-name> Sets the output assembly name. Defaults to the input filename without its extension, or the folder name.
--publish-code Also saves the generated C# as <assembly-name>.cs.

When using both named options, put --output-directory before --name, as shown above; this matches the current CLI command patterns. The --publish-code flag can accompany any build command.

The compiler emits a console application when a static Main method is present, and a library otherwise. Use a valid C#-compatible entry-point signature, such as the Main above. A library has no entry point and cannot be launched with dotnet; it is intended for use by other .NET code.

On successful compilation, the CLI reports whether it produced a console application or library and prints the output paths. Compilation errors go to standard error with exit code 1; an unrecognized command prints usage with exit code 2.

Building a folder

Build all .kobo files directly inside a folder into one assembly:

kobo build ./src

The same build command handles file paths and folder paths automatically; there is no separate folder-build command. Omit the path to build the current directory, optionally including its subfolders:

kobo build
kobo build --recursive
kobo build --output-directory ./build --name MyApp --publish-code

Include subfolders of an explicit folder with --recursive:

kobo build ./src --recursive --output-directory ./build --name MyApp --publish-code

Files share a compilation context, including one generated Global class and one definition of each generated union per namespace. Without --recursive, subfolders are not searched. Discovery is case-insensitive for .kobo extensions and files are processed in ordinal path order. Recursive discovery skips bin, obj, and symbolic-link directories.

Output defaults to the input folder, with the folder name as the assembly name. The output options work for file and folder inputs; when both are specified, --output-directory must precede --name. --recursive and --publish-code can appear anywhere after the command. A missing/unreadable folder, no matching files, or a compilation failure reports an error and exits with code 1. Any static Main in the combined sources selects a console application; otherwise the result is a library.

From a source checkout, run the unified file/folder build regression checks with dotnet run --project KoboLang.TestHarness -- --folder-build-self-test.

Language guide

For a complete Kobo-first introduction, see The Kobo Programming Language. Standalone single-file builds accept #:sdk, #:property, and #:package; these are passed to the SDK build. Other app directives, including #:project, are rejected. Multi-file builds do not accept app directives.

Kobo reuses familiar .NET types, expressions, method calls, and brace-delimited blocks while providing its own declaration syntax. Explicit types are useful at API boundaries; local variables can use inference.

Multiline LINQ queries

LINQ query expressions spanning multiple lines must end their statement with an explicit semicolon, including when the query is parenthesized:

var items = select item from args
            where item.Length > 0
            project item;

Omitting the semicolon reports a KOBO0001 error at the query's select keyword. Single-line queries retain optional semicolons.

Select-first LINQ syntax

Kobo also accepts the following query syntax, lowered to ordinary C# LINQ:

select <identifier> from <expression>
    [join <identifier> from <expression> on <left> == <right>]*
    [where <condition>]
    [group <expression> by <expression> [into <identifier>]]
    [orderby <expression> [asc|desc]]
    [project <expression>]

Clauses must appear in this order. Without project, the query selects the initial identifier. A group clause returns groups; use into name to refer to them in subsequent orderby or project clauses. Without project, a group continuation selects that group identifier. Ordering defaults to ascending. Join conditions must be equality expressions and become C# on left equals right.

var results = select person from people
              join order from orders on person.Id == order.PersonId
              where person.Age > 30
              orderby order.Total desc
              project {
                  person.Name,
                  order.Total
              };

project { ... } creates an anonymous object (select new { ... }). Other projections, such as project person.Name, become ordinary C# select clauses. From-first from ... in ... query expressions are rejected, including nested queries. Query statements require an explicit terminating semicolon when spanning multiple lines.

Imports and modules

Use / to separate namespace segments:

import * from System
import * from System/Collections/Generic
import * from System/Net/Http

module Examples/Collections

import ... from ... emits file-level C# directives without rewriting type references. Wildcards become namespace directives and can be grouped:

import * from
{
    System/Net,
    System/Collections/Generic,
    System/Linq
}

import * as Sys from System becomes a namespace alias. import HttpClient as Client from System/Net/Http becomes a C# type alias, and compound imports can mix aliases and per-entry static imports: import { static Math, Console as Output } from System. Static wildcards are not supported.

Use using Type [as Alias] from Namespace/Path for explicit fully qualified type replacement, including class/function/block scopes. For example, using Math as M from System turns M.Sqrt(144) into global::System.Math.Sqrt(144). Compound lists and open generic definitions are supported by using; import cannot alias an open generic definition. Both APIs and native scripts follow these same rules. Old import forms are rejected. See the imports guide for all forms and migration examples. A module becomes a file-scoped namespace, so module Examples/Collections maps to namespace Examples.Collections;.

Types, modifiers, and inheritance

Union syntax is restricted to variable types, such as var:union(int,string) value = 1. Standalone declarations like union:Hello {} are not supported and report a Kobo error.

Declare types with class:Name, struct:Name, interface:Name, or record:Name. Put modifiers and attributes in square brackets before the declaration. Use is for inheritance:

is is the only supported form for inheritance and interface implementation. Separate multiple bases with commas. Colon-based base lists, inherits, extends, and implements are rejected. Generic where constraints retain their constraint colon.

interface:IGreeter
{
    function:void Greet()
}

[public, abstract] class:BaseGreeter is IGreeter
{
    [public, abstract] function:void Greet()
}

[public] class:Greeter is BaseGreeter
{
    [public, override] function:void Greet()
    {
        Console.WriteLine("Welcome!")
    }
}

Modifier lists can include declarations such as public, static, abstract, virtual, and override. Attributes can also appear in the list, for example [public, AttributeUsage(AttributeTargets.Class)] on an attribute class.

Functions, parameters, and generics

Always use function:<return-type> <name>(<parameters>):

[public, static] function:int Add(int left, int right)
{
    return left + right
}

[public, static] function:string Describe(string value)
{
    return value
}

[public, static] function:T Identity<T>(T value) where T : struct
{
    return value
}

Parameters must use Type name; name: Type declarations are rejected. Generic type parameters and C#-style where constraints are supported on types and functions.

Function declarations require an explicit return type, including void for functions that return nothing. func is not a supported declaration keyword, and function Name(...) -> Type is not the supported function form.

Variables, constants, and collections

var:int count = 3
var message = "Kobo"
const:int limit = 10
var:List<int> numbers = [1, 2, 3]
var names = ["Ada", "Grace", "Linus"]

Use var:Type for explicit typing and var for inferred local values. Untyped var fields outside functions are translated to dynamic; prefer explicit field types when you want compile-time checking. Constants must meet the generated C# compile-time constant rules.

Collection literals use square brackets. Kobo can infer collection element types in supported cases, but explicit types make the intended collection shape clearer.

Properties and indexers

[public] class:Person
{
    var:List<string> _names = ["Ada", "Grace"]

    [public] property:string? Name { get; set; }
    [public] property:int Age
    [public] property:int DoubledAge => Age * 2

    [public] indexer:string Names[int i] => _names[i];
}

Properties support accessor blocks, shorthand auto-properties, and expression bodies. Nullable types such as string? can be used in typed declarations.

Indexers use indexer:Type Name[parameters]. Named indexers generate helper nested types; the this form maps to a conventional C# indexer.

Control flow and statement layout

var total = 0
var numbers = [1, 2, 3]

foreach number in numbers
{
    total += number
}

if total > 0
{
    Console.WriteLine(total)
}

while total > 0
{
    total -= 1
}
  • Semicolons are optional at the end of single-line statements; explicit semicolons are also accepted.
  • Parentheses around if, else if, and while conditions can be omitted when the condition ends on that line and the body follows.
  • A condition and its statement on the same line require parentheses: if (ready) return value.
  • foreach name in items, foreach var name in items, and foreach var:int name in items are supported, as are parenthesized headers.
  • Braces can appear on the next line. Use braces to make multi-statement blocks explicit.
  • Catch declarations have optional parentheses: catch Exception error and catch (Exception error) are equivalent. Type-only catch Exception and bare catch are supported. Bodies require braces; filters retain parentheses, for example catch Exception error when (error.Message.Length > 0).
  • Use // for single-line comments.

Lambdas, delegate types, and trailing blocks

Delegate types use (<parameter-types>)-><return-type>:

Kobo type Generated .NET delegate
(string)->bool Func<string, bool>
(int,string)->void Action<int, string>
()->void Action

Inline lambdas can have expression or block bodies:

var:(int)->int doubled = x => x * 2
var answer = doubled(21)

A function whose last parameter is a delegate can be called with a trailing lambda block:

[static] function:string Search(List<string> names, (string)->bool filter)
{
    foreach name in names
    {
        if filter(name)
        {
            return name
        }
    }

    return "<not found>"
}

Call it from another function:

var names = ["Ada", "Grace", "Linus"]
var result = Search(names)
{
    name => name == "Grace"
}

The trailing block supplies the final delegate argument rather than starting a separate statement.

Updating an object with scope

Use scope to avoid repeating a receiver when updating its members:

var person = new Person()
scope(person)
{
    .Name = "Ada"
    .Age = 36
}

Leading member dots in the scope are rewritten using the receiver, for example .Age = 36 becomes person.Age = 36.

The body is a regular block and can contain variables, conditionals, loops, function calls, and other logic. A scope cannot appear inside another scope, including within a conditional or loop in its body; nested scopes are rejected.

Type extensions

Type extensions can declare methods, properties, and indexers for an existing type inside an extension(Type receiver) block.

extension(string text)
{
    [public] function:int CharacterCount()
    {
        return text.Length
    }

    [public] property:bool IsEmpty => text.Length == 0

    [public] indexer:char this[int index] => text[index];
    [public] indexer:char Characters[int index] => text[index];
}

The named indexer supports "Kobo".Characters[0], which returns 'K'.

Extension blocks generate members in a shared static partial class Extensions, which is internal by default. Use [public] extension(string text) to make that class public. Only public is allowed on the extension block itself, and all extension blocks in a file must have the same effective access modifier. Individual members still use their own modifier lists.

Syntax rules to remember

Keep declaration colons adjacent to both the keyword and type/name: class:Person, function:void, and var:int, not class: Person or var :int. Parameters use Type name, without a colon. Generic constraints use where T : struct.

Use Kobo declaration forms rather than C# forms such as class Person. Kobo is a preview transformer, not a promise of complete C# syntax compatibility. If an expression or declaration fails to compile, inspect diagnostics and use --publish-code to see the generated C#.

Editor workflow

The Kobo VS Code extension recognizes .kobo files and launches the Kobo language server for semantic highlighting and transform diagnostics. It does not compile or run your program; use the CLI separately.

The extension requires VS Code 1.85 or later and the .NET 11 runtime. If dotnet is not on your PATH, configure kobo.server.dotnetPath. Server output is available in the Kobo Language Server output channel.

If the tool is already installed, use dotnet tool update instead of dotnet tool install.

Further reading

License

This package is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.12 0 10/28/2026
0.1.10-preview.1 25 10/28/2026
0.1.8-preview.1 74 10/14/2026
0.1.7-preview.1 54 10/12/2026