npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kopscript

v0.7.0

Published

KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types

Readme

KopScript

KopScript is a small, strongly-typed, object-oriented programming language that transpiles to readable JavaScript. It's a hobby/learning project: a from-scratch lexer, parser, type checker, and code generator, written in TypeScript — leaning deliberately toward C# paradigms (type-first declarations, interfaces, explicit virtual/override dispatch, auto-properties, enums) rather than TypeScript ones.

Because KopScript compiles to plain JavaScript and runs on Node, it runs identically on Windows, macOS, and Linux — there's no native toolchain to maintain.

Generating KopScript with an AI coding assistant? Point it at LLM.md — a dense, complete, example-driven language spec designed to be loaded straight into an LLM's context, as opposed to this README's narrative explanation.

Highlights

  • Object-oriented, C#-flavored: type-first declarations (string Name;, not name: string), classes with auto-properties, single inheritance plus interfaces via class Dog : Animal, IPet, explicit virtual/override dispatch (methods are sealed unless marked virtual), and a full public/protected/private access model enforced at compile time.
  • Strongly typed: every declaration is explicitly typed and checked at compile time.
  • Generics: a single, unconstrained, invariant type parameter on classes and interfaces (class Box<T> { public T Value; }) — erased at codegen with zero runtime cost, the same way task<T>/state<T> already are.
  • Nullable types with compiler-enforced null-checking: T? (string?, Dog?, number[]?) — a T? can't be used where a T is expected without an if (x != null) check first (checked statically, not just at runtime), and comparing a value that can never be null to null is itself a compile error.
  • Built-in string pattern matching: a match expression over strings supporting literal, wildcard, and regex patterns — no if/else chains required.
  • Real multi-file programs: using "./shapes"; compiles a whole dependency graph together, checking public/private visibility across files and emitting genuine ES import/export statements — so a compiled KopScript module is also a normal JS module that a plain Node/TS project can import directly.
  • First-class functions and real closures: lambdas ((number x) => x * 2), function types ((number, number) => number), and calling any function-valued expression — not just named functions — compiling straight to native JS arrow functions, so closures work exactly like they do in JS.
  • JS/npm interop: extern declarations describe the shape of an existing JS function, class, or global value — ambient (Math, document, no import) or imported from a module specifier — so KopScript code can call real JS APIs, including a virtual method on an extern class that a real KopScript class can override, which is what lets a class subclass something from another package/repo entirely.
  • async/await: task<T> (a promise of a T) compiles to a real JS Promise, async/await to the real thing, and try/catch/finally/throw give a rejected task something to catch.
  • Reactive state without RxJS: state<T> is a small reactive box (.Value get/set, .Subscribe((T) => void)) for holding state and reacting to it changing — no Observables, no operators, no manual unsubscribe bookkeeping.
  • Real markup templates: template from "./x.html"; compiles a separate, almost-pure-HTML file — interpolation, event/property bindings, *if/*for — down to the exact same AST a hand-written Render() would produce, with auto-Subscribe wiring for state referenced directly in the markup. See Templates.
  • A companion framework, Kopular: components, constructor-injected services via a composition root (no DI container), and real-URL routing (no config DSL) — built entirely on the features above, in a separate repo consumed as a real package. KopularDemo is a real app built on both.

Getting started

npm install
npm run ks -- run examples/animals.ks
npm run ks -- run examples/classify.ks
npm run ks -- run examples/features.ks
npm run ks -- run examples/shapes.ks
npm run ks -- run examples/modules/main.ks
npm run ks -- run examples/closures.ks
npm run ks -- run examples/async.ks
npm run ks -- run examples/arrays.ks

These are all plain console scripts, runnable with Node directly. A browser-facing KopScript project (DOM extern bindings, ks watch + a static file server for a live reload-free dev loop) is a different setup — see Kopular and KopularDemo for a real example of that workflow.

Or build to a .js file without running it:

npm run ks -- build examples/animals.ks

Run the test suite:

npm test

Language tour

Variables

Declarations are type-first, C# style — the type comes before the name, with no let:

number x = 5;
const string name = "Joe";

Functions

Free functions are also type-first, with the return type before the name (no function keyword — the same shape as a class method, just outside a class):

number Add(number a, number b) {
  return a + b;
}

void Announce(string message) {
  print(message);
}

Functions as values, and closures

A function type is written (ParamType, ...) => ReturnType. Lambdas always have explicit parameter types (no inference in v1) and never write their own return type — it's checked against whatever function type the lambda is used where, which KopScript always knows concretely since every declaration is explicitly typed:

number Apply((number) => number f, number x) {
  return f(x);
}

number Square(number x) { return x * x; }

print(Apply((number x) => x * 2, 5)); // 10 — an inline lambda
print(Apply(Square, 4));               // 16 — a named function used as a value

A lambda body can be an expression ((number x) => x * 2) or a block ((number x) => { return x * 2; }). Lambdas are real closures — they capture their enclosing scope by reference, compiling directly to JS arrow functions, so mutating a captured variable from inside a lambda is visible to the code that captured it:

number counter = 0;
() => number next = () => { counter = counter + 1; return counter; };
print(next()); // 1
print(next()); // 2

Calling any expression whose type is a function type works — a variable, a field, a call that returns a function, not just a named function or method.

Classes, auto-properties, and inheritance

class Animal {
  public string Name { get; set; }
  constructor(string name) { this.Name = name; }
  public virtual string Speak() { return this.Name + " makes a sound"; }
}

class Dog : Animal {
  public override string Speak() { return this.Name + " barks"; }
}
  • public string Name { get; set; } is a read-write auto-property; public string Name { get; } is get-only — it can only be assigned within its own class's constructor (like a C# read-only auto-property), and is rejected anywhere else, including other methods of the same class.
  • public / protected / private are enforced at compile time: private members are only reachable from within the declaring class (any instance, not just this); protected members are reachable from the declaring class and its subclasses; public is unrestricted. Fields default to public if no modifier is written.
  • Methods are sealed by default, like C#. A subclass may only redefine a method that the base class marked virtual, and must mark its own version override; the compiler rejects a redefinition that omits override, an override with no matching virtual base method, or a signature mismatch between the two.
  • static fields and methods belong to the class itself, accessed as ClassName.Member (never through an instance or this) and shared across every instance. A static field requires an inline initializer, since there's no constructor to assign it in: private static number Count = 0;. static cannot be combined with virtual/override, and static auto-properties aren't supported in v1 — use a static field instead.
  • A subclass that doesn't declare its own constructor inherits the superclass's (the same way JavaScript's extends works). Declaring a constructor in a subclass that extends another class is not supported in v1.

Interfaces

class Foo : Base, IBar, IBaz mixes at most one base class with any number of interfaces in a single colon-separated list — the compiler figures out which name is which. A class implementing an interface must provide every method the interface declares, with a matching signature. Interfaces can themselves extend other interfaces (interface INamedShape : IShape { ... }), which pulls in the parent's method requirements too; implementing classes must satisfy the whole chain. Interfaces only declare method signatures in v1 — no properties.

interface ISpeaker {
  string Speak();
}

class Dog : ISpeaker {
  public string Speak() { return "Woof"; }
}

void Announce(ISpeaker s) {
  print(s.Speak());
}

Generics

Classes and interfaces can take a single type parameter:

class Box<T> {
  public T Value;
  constructor(T v) { this.Value = v; }
  public T Get() { return this.Value; }
}

Box<number> nb = new Box<number>(5);
Box<string> sb = new Box<string>("hi");
print(nb.Get());   // 5

The type argument is required everywhere the generic type is named — on the variable's declared type and separately on new (Box<number> nb = new Box<number>(5);, not just one or the other). Generics are erased at codegen exactly like task<T>/state<T> already are — Box<number> and Box<string> compile to the identical plain class Box, so there's no runtime cost.

This is deliberately the smallest useful slice of generics, not a scaled-down promise of more later inside v1:

  • One type parameter, invariant, unconstrained. No Map<K, V> (multiple parameters), no T : ISomething (constraints) — and because T is fully unconstrained, you can't call any member on a bare T value inside the generic class's own body (that's correct behavior, the same restriction C#/Java/TypeScript put on an unconstrained parameter, not a bug). Invariant means Box<Dog> is not assignable to Box<Animal> even though Dog : Animal — type arguments must match exactly.
  • No generic functions. T Identity<T>(T x) isn't supported — only classes and interfaces take a type parameter. A method inside a generic class can still use that class's own T freely; it's just not introducing a type parameter of its own.
  • No generic inheritance. A class or interface's base list can only name a non-generic type. class Foo : Box<number> and class Foo<T> : SomeBase<T> are both compile errors — a generic class can still extend/implement ordinary non-generic bases normally, it just can't be the one on either side of a generic base relationship.

See LLM.md's "Generics" section for the exhaustive rules if you're generating code against this — nested generics (Box<Box<number>>, Box<number>?[]) and generic interfaces used as standalone parameter types both work and are covered there.

Enums

enum Color { Red, Green, Blue }

Color c = Color.Green;
if (c == Color.Green) { print("It's green"); }

Members are numbered from 0 in declaration order, compiling to a frozen JS object.

Modules

using "./shapes"; brings every public top-level declaration from that file (resolved relative to the current file, no .ks extension) into unqualified scope — there's no namespace prefix and no picking individual names, the same way a C# using directive brings a whole namespace into scope. using directives must be a contiguous block at the very top of the file.

// shapes.ks
interface IShape {
  number Area();
}

class Circle : IShape {
  public number Radius;
  constructor(number radius) { this.Radius = radius; }
  public number Area() { return this.Radius * this.Radius * 3; }
}
// main.ks
using "./shapes";

Circle c = new Circle(2);
print(c.Area());

Every top-level class, interface, enum, and function is public (exported, visible to files that using this one) by default — mark it private to keep it file-scoped:

private number Helper() { return 42; } // not visible outside this file
class Public { }                        // visible by default

Top-level let/const variables are never exportable — only types and functions cross file boundaries. There's also a structural rule worth knowing: if a class or interface is exported, everything in its public surface must be exported too (an exported class's base class and implemented interfaces, an exported interface's base interfaces) — the compiler rejects public class Derived : Base if Base isn't also public, since otherwise a file importing Derived would have no way to make sense of its own base type.

ks build/ks run compile the entry file plus everything it transitively usings, each to its own .js file with real ES import/export statements — so node main.js (or a plain JS/TS project importing the compiled output directly) just works, with Node's own module resolution doing the wiring.

Interop with JS/npm (extern)

extern declarations describe the shape of something that already exists in JS, without providing a KopScript implementation — KopScript trusts the declared types (same trust model as a TypeScript .d.ts file). There are three forms, and each can be either ambient (no from — an already-existing global like Math or document, nothing to import) or imported (from "<module>" — a named export from an npm/Node module):

extern string ReadFileSync(string path) from "node:fs" as "readFileSync"; // function
extern Document document;                                                 // value (ambient)

extern class Element {          // class — a type shape, describing an existing class
  constructor();
  string textContent { get; set; }
  void addEventListener(string eventType, (Event) => void handler);
  static number InstanceCount { get; }
} from "some-dom-lib";

as "<jsName>" maps a KopScript-facing name to the real JS-side identifier (defaults to the declared name if omitted) — needed constantly in practice, since JS naming (camelCase, readFileSync) rarely matches KopScript's (PascalCase, ReadFileSync). Extern class members have no separate rename mechanism, though — write them with the exact real JS name (addEventListener, not AddEventListener), since that's what actually exists at runtime. Calling an extern method or accessing an extern property compiles exactly like any other member access — no special codegen, since it isn't reimplementing the class, just describing one that's already there.

A real, working example: Kopular's dom.ks declares ambient bindings for document/Element/Event/window/location, which its Component/Router are built on — and KopularDemo consumes Component/Router themselves via extern class ... from "kopular/...";, proving extern works as a real cross-package boundary, not just for describing DOM globals within a single project.

An extern class can carry its own type parameter, extern class Box<T> { ... } — exactly the same single-type-parameter rules as a real generic class (see "Generics" above: invariant, unconstrained, erased, can't appear in a base list), so a generic type from another package (e.g. Kopular's FormField<T>) can be described and instantiated generically, not just per concrete type:

extern class Box<T> {
  constructor(T v);
  T Value { get; }
} from "some-package";

Box<number> nb = new Box<number>(5);

Now that KopScript has async/await and task<T> (see below), a Promise-based JS API is describable too — extern task<string> Fetch(...) from "..." as "fetch"; is legitimate, and awaiting it works exactly like awaiting any other KopScript task. What's still not cleanly describable is old-style Node callback-based async that doesn't return a Promise at all (fs.readFile(path, callback) with an error-first callback) — nothing in KopScript understands that specific convention, even though the callback parameter itself is describable as an ordinary function type.

Compile-time file embedding (raw)

raw string <Name> from "<path>"; reads the file at <path> (resolved relative to the .ks file that declares it) at compile time and embeds its contents as a plain string constant — no fetch, no runtime file access, nothing left over at all once compiled. Top-level only, always string, exported by default like any other top-level declaration (private keeps it file-scoped):

raw string CounterHtml from "./counter.html";

void Main() {
  print(CounterHtml); // the file's exact contents, as a string
}

Unlike extern, there's no real JS export being described here — the compiler fabricates the value itself — so there's no as "jsName" clause. This is a general-purpose, compile-time-only file embedding primitive — useful for anything that wants a file's exact contents as a string constant with no runtime file access. (If what you actually want is markup that becomes real DOM construction code, not just a string — see Templates below, which is the more specific answer for that case.)

async/await, task<T>, and try/catch

task (a promise of nothing) and task<T> (a promise of a T) are the one hardcoded parametrized type in v1 — not general generics, just enough to give async/await a return type. async requires (and is required by) a task/task<T> return type; return statements inside an async body are checked against the unwrapped result type, exactly like real C#/JS async functions — you write return 5;, not return SomeTaskOf(5);:

async task<number> Double(number x) {
  return x * 2;
}

async task<number> Chain(number x) {
  number a = await Double(x);
  number b = await Double(a);
  return b;
}

Both compile directly to their real JS equivalents (async function, await), so an async KopScript function returns a genuine Promise and composes with plain JS/TS code without any wrapping. await also works at the top level of a file, matching real top-level await in an ES module.

Exception handling exists specifically so a rejected task has something to catch:

try {
  number result = await MightFail(x);
  print(result);
} catch (string message) {
  print("caught: " + message);
} finally {
  print("done");
}

throw accepts any type — there's no base exception/error type to require conformance to, matching JS's own looseness — and a catch (Type name) parameter's type is trusted, not verified (the same trust model as extern: KopScript has no way to know what a given throw site might actually produce). At least one of catch/finally is required; a bare try {} alone is rejected. examples/async.ks and test/codegen.test.ts exercise a full chain of async calls, a caught error, and a finally that runs on both the success and failure path.

Known v1 limitations: no async lambdas (await is never valid inside a lambda body, even inside an async function — only free functions and methods can be async), and no way to construct a task value directly outside of an async function body.

Reactive state: state<T>

state<T> is the other hardcoded parametrized type in v1, alongside task<T> — a reactive box holding a T. Construct one with state(initial) (no explicit type argument; T is inferred from initial), read/write it through .Value, and register a listener with .Subscribe((T) => void) that fires every time .Value is assigned:

state<number> count = state(0);
count.Subscribe((number v) => print("now " + v));
count.Value = count.Value + 1; // prints "now 1"

This exists to give Kopular components a way to re-render on state change without pulling in anything like RxJS — no Observables, no operators, no manual unsubscribe bookkeeping. A component subscribes once, in its constructor, to call its own Update():

class Counter : Component {
  private state<number> Count;

  constructor() : base() {
    this.Count = state(0);
    this.Count.Subscribe((number v) => this.Update());
  }

  public override Element Render() {
    Element button = document.createElement("button");
    button.textContent = "Count: " + this.Count.Value;
    button.addEventListener("click", (Event e) => {
      this.Count.Value = this.Count.Value + 1; // Update() fires automatically
    });
    return button;
  }
}

Compare this to the pre-state<T> version of the same component, which had to call this.Update(); by hand inside every single event handler that touched state — one Subscribe call at construction now does that job everywhere. Kopular's Component is built around exactly this pattern, and its own README covers the rest of the "Angular's separation, none of the bloat" story — constructor-injected services instead of a DI container, and hash-based routing instead of a config DSL — both built entirely on ordinary KopScript, no further compiler features required beyond state<T> and the extern/virtual support above.

Hand-writing Render() like this, statement by statement, is still fully supported — but a Kopular component today more commonly expresses it as a template instead. See Templates below for the markup-based alternative to this same method, and for how Count.Subscribe(...) above can often be skipped entirely.

Templates

template from "<path>"; inside a class body replaces a hand-written Render() with a real, separate markup file — almost pure HTML, with data bindings and a small set of structural directives standing in for the imperative DOM code above:

class Counter : Component {
  public state<number> Count;
  constructor() : base() { this.Count = state(0); }
  public void Increment() { this.Count.Value = this.Count.Value + 1; }
  template from "./counter.html";
}
<!-- counter.html -->
<button (click)="Increment()">Count: {{ Count.Value }}</button>

This compiles to exactly the Render() method you'd otherwise write by hand — the template compiler is a pass that runs between parsing and type-checking, turning the markup into ordinary MethodDecl/statement/expression AST nodes and splicing the result into the class before checking ever runs. There's no separate runtime template engine, no virtual DOM diffing, and no interpreted expression language: {{ Count.Value }} and (click)="Increment()" contain real KopScript, parsed and type-checked exactly like anything else in the file, with errors reported at their real position in the .html file, not the .ks file.

A class may have a template or a hand-written Render(), never both — that's a compile error. ks watch also tracks the referenced .html file, so editing markup alone triggers a rebuild.

Supported bindings and directives:

| Syntax | Desugars to | | --------------------------- | --------------------------------------------------------- | | {{ expr }} (in text) | el.textContent = $"...{expr}..."; (an InterpolatedStringLiteral, same as $"...") | | (event)="stmt" | el.addEventListener("event", (Event e) => { stmt }); | | [prop]="expr" | el.prop = expr; — a plain assignment, checked like any other | | class="..." (static) | el.className = "..."; (aliased, since class is a KopScript keyword) | | *if="expr" | a real if (expr) { ... } around the element's creation | | *for="Type var of expr" | a real for (Type var in expr) { ... } — the element type is explicit, matching KopScript's no-inference stance elsewhere (Generics, Nullable types) |

Auto-subscribe: a state<T> field declared directly on the component and referenced directly in its template (like Count above) gets its Subscribe((v) => this.Update()) wired up automatically — no manual Subscribe call needed in the constructor. This is a syntactic check against the class's own declared fields, not a type-checker query, so it only covers direct field access; state reached indirectly — through a method call, or through this.SomeService.Count — still needs a manual Subscribe, same as before templates existed.

v1 cuts, same discipline as generics and nullable types: exactly one top-level element per template (no auto-wrapping — a clear error instead); no mixing text and element children under one element (Kopular's DOM surface has no text-node type, only .textContent); no two-way binding, no pipes, no stacking two structural directives on one element.

A deliberate layering note: the template from syntax lives in kopscript's own grammar (Kopular can't extend a language it doesn't own), but what it desugars todocument.createElement, .appendChild, .textContent, .addEventListener — assumes exactly the DOM surface Kopular's dom.ks declares. That's a real coupling from the compiler to one specific consumer, accepted deliberately rather than building a generic pluggable desugaring-target system for a hypothetical second framework that doesn't exist today. If one ever does, that's the point to generalize this.

raw string <Name> from "./x.html"; (above) still exists as a general-purpose compile-time file embedding feature — but for the specific "component markup in its own file" use case, template from is the real answer; raw no longer needs to stand in for it.

String interpolation

string name = "Joe";
string msg = $"Hello, {name}!";

Pattern matching over strings

The headline feature: a match expression with literal, comma-separated, and regex patterns. A match must end with a _ wildcard arm.

string Classify(string input) {
  return match input {
    "cat", "dog" => "animal",
    r"^[0-9]+$" => "number",
    _ => "unknown"
  };
}

String stdlib

KopScript's string type exposes PascalCase members that map directly onto String.prototype:

| KopScript | JavaScript | | ----------------------- | -------------------------- | | s.Contains(x) | s.includes(x) | | s.StartsWith(x) | s.startsWith(x) | | s.EndsWith(x) | s.endsWith(x) | | s.Replace(a, b) | s.replaceAll(a, b) | | s.Split(x) | s.split(x) | | s.Trim() | s.trim() | | s.ToUpper() | s.toUpperCase() | | s.ToLower() | s.toLowerCase() | | s.Length | s.length |

Array stdlib

Arrays expose .Length, plus Map/Filter/ForEach/Push, using lambdas or any other function-valued expression (a named function, a variable holding one, ...):

number[] xs = [1, 2, 3, 4];
string[] labels = xs.Map((number x) => "n" + x);      // ["n1", "n2", "n3", "n4"]
number[] evens = xs.Filter((number x) => x % 2 == 0);  // [2, 4]
xs.ForEach((number x) => print(x));
number[] grown = xs.Push(5);                           // [1, 2, 3, 4, 5]; xs itself is untouched

Map/Filter/ForEach compile straight to their real Array.prototype equivalents. Push is the one departure from JS: it's non-mutating (returns a new array; xs itself is unchanged), unlike JS's own Array.prototype.push — chosen for consistency with Map/Filter (already non-mutating) and because nothing else in KopScript's type system models aliasing/mutable-reference semantics, so a silently-mutating Push would be a surprising outlier. It compiles to a plain spread ([...xs, 5]), not a .push() call.

Map's result type is the one genuinely polymorphic piece of the whole language — the result element type is whatever the callback actually returns, not a fixed signature. A plain function reference (xs.Map(SomeFunction)) already carries a fully-known type, so that case is exact; an inline expression-bodied lambda (xs.Map((number x) => ...)) has its return type inferred from the body. A block-bodied lambda passed to Map is a known v1 gap — its result type can't be inferred that way yet.

Control flow

if (x > 0) {
  print("positive");
} else {
  print("non-positive");
}

while (x < 10) {
  x = x + 1;
}

for (number i = 0; i < 10; i = i + 1) {
  print(i);
}

foreach (number item in items) {
  print(item);
}

foreach requires an explicit element type (there's no var/type inference in v1). break and continue work inside while, for, and foreach loops.

Types (v1 scope)

number, string, bool, void, T[] (arrays), class types, interface types, enum types, function types ((T, ...) => R), nullable types (T?, with null and compiler-enforced null-checking — see "Nullable types" below), and a single type parameter on classes/interfaces (Box<T> — see "Generics" above).

Nullable types — T?

string? maybeName = null;
void Greet(string? name) {
  if (name != null) {
    print("Hello, " + name);   // name is `string` here, not `string?`
  } else {
    print("Hello, stranger");
  }
}

? is a postfix modifier on any type (Dog?, or an array either way — string?[] is an array of nullable strings, string[]? is a nullable array of strings; order matters). It's purely a compile-time distinction — erased at codegen, since JS already has native null. The compiler enforces it in both directions:

  • A T? can never be used where a T is expected — accessing a member or index on one is a compile error — unless it's been narrowed by an if (x != null) / if (x == null) {...} else {...} check (either operand order, and inside a &&/|| short-circuit) on that exact local/parameter. Narrowing doesn't survive past the checked block, doesn't follow a this.Field path (locals/params only), and doesn't understand an early-return guard clause (if (x == null) { return; }) — each of those would need real control-flow analysis, deliberately not taken on in v1.
  • Comparing a value to null when its type can't be null (i.e. isn't itself a T?) is a compile error, not a silently-always-false comparison — a real static check, not just a style rule.

See LLM.md's "Nullable types" section for the exhaustive rules if you're generating code against this.

Built-ins

print(...) compiles to console.log(...).

Architecture

src/
  lexer.ts       tokenizer: source -> Token[]
  tokens.ts      token kind enum + Token type
  ast.ts         AST node type definitions
  parser.ts      recursive-descent parser: Token[] -> AST (Program)
  diagnostics.ts error/warning collection with line/col + source snippets
  types.ts       type system: Type representations + compatibility rules
  checker.ts     semantic analysis: scopes, symbol table, type checking over the AST
  codegen.ts     AST -> JavaScript source string (readable ES2020 output)
  modules.ts     multi-file orchestration: resolves the `using` graph, checks
                 modules in dependency order, drives codegen across all of them
  cli.ts         `ks build|run|watch|check <file>` entry point
examples/        sample .ks programs (examples/modules/ is a multi-file one)
test/            vitest unit and end-to-end tests

Pipeline, per file: source -> lexer -> parser (AST) -> checker (validates the AST, collects diagnostics) -> codegen (emits JS). modules.ts sits above this: it parses the entry file and everything it transitively usings, topologically sorts them (dependencies first), and checks each one with its direct dependencies' exports seeded in — so a class from another file resolves exactly like a local one once the checker starts. Codegen then runs per file, turning each using into a real import statement and each exported top-level declaration into a real export. The CLI aborts before writing anything if any file in the graph has errors, and otherwise writes one .js file next to each .ks source and (for run) executes the entry file's output with node.

Interfaces and the virtual/override discipline are purely compile-time: JS methods are always dynamically dispatched, so codegen doesn't need to do anything special for either one — the checker just validates the contract before code is ever emitted. Enums compile to a small frozen object (Object.freeze({ Red: 0, Green: 1, ... })); interfaces have no runtime representation at all and are dropped from the emitted JS entirely.

CLI

ks build <file.ks>   # type-check and emit <file>.js next to the source
ks run <file.ks>     # build, then execute the emitted JS with node
ks watch <file.ks>   # build, then rebuild on every change to any file in the graph
ks check <file.ks>   # type-check only — no output files written

build and check both accept --json, which replaces all human-readable output with a single JSON object on stdout — meant for CI or a tool/agent parsing the result programmatically instead of scraping formatted text:

{
  "success": false,
  "diagnostics": [
    { "severity": "error", "message": "Argument 2 has type 'string', expected 'number'", "line": 2, "col": 14, "file": "/abs/path/to/file.ks" }
  ],
  "written": []
}

written lists the absolute paths of every .js file actually written (always empty for check, and for build on failure — nothing is written unless the whole graph is error-free). A missing entry file reports { "success": false, "diagnostics": [], "written": [], "error": "cannot find file '...'" } instead of throwing. Exit code is 0 exactly when success is true, both with and without --json.

During development, use npm run ks -- <build|run|watch|check> <file.ks> (backed by tsx), or run npm run build to compile the TypeScript compiler itself to dist/ and use node dist/cli.js directly.

watch rebuilds on a save to any .ks file it reached while compiling — the entry and everything it (transitively, non-transitively per-file) usings — not just the entry file, plus any .html file referenced via template from "..."; in one of those classes, and re-establishes its watch list after every rebuild since the dependency set itself can change (a using added or removed, or a template declaration's path). This is the piece that makes a browser-facing dev loop bearable: run ks watch in one terminal, a static file server in another, and refreshing the browser after a save is the only manual step left — there's no watch-triggered auto-refresh, since KopScript has no dev-server integration to push that to the page. See KopularDemo for this workflow in practice.

Editor support

editors/vscode/ is a local-install-only VS Code extension providing .ks syntax highlighting (a TextMate grammar — comments, all three string forms including highlighted interpolation expressions, keywords, types, function calls) and bracket/comment configuration. No language server, no Marketplace listing — see editors/vscode/README.md for installing it locally (Developer: Install Extension from Location..., or symlink it into your extensions folder).

Status

This is a v1 / hobby-project scope. Nullable types (T?) and generics (a single unconstrained, invariant type parameter on classes/interfaces) have both shipped — see above for both. What generics deliberately doesn't cover: multiple type parameters (Map<K, V>), constraints (T : IFoo), generic functions, generic inheritance, and variance — each a real, separable extension rather than a v1 oversight. Also not yet supported: static auto-properties, interface properties (methods only), and nested functions/classes.

async/await, task<T>, and try/catch/finally/throw are now in place (see the language tour above) — the ceiling that's left is what's inside those: no async lambdas, and no way to build a task value by hand outside an async function body.

A frontend framework: Kopular

The language shape — modules, closures, DOM interop, state<T>, and extern/virtual subclassing across a package boundary — exists to support real UI components, not just scripts. That framework itself, Kopular, lives in its own repo, published to npm as kopular and consumed like any other package (no local checkout or file: dependency needed). A real app built on both lives in KopularDemo — several routed pages, state<T>-driven reactivity, constructor-injected services via a composition root (no DI container), real-URL (History API) routing, and Angular-style markup templates (see Templates above) with *if/*for structural directives — verified against a real DOM via jsdom and in an actual browser.