Skip to content
Lucent
SEARCH LUCENT

Search guides, APIs, and examples.

GitHub

Very early and experimental. The language, the generated native code and every package API change without notice. Do not use Lucent in production.

Diagnostics

Every LUCENT code, with its reason, its fix, and a wrong and a right example.

terminal
error LUCENT2001 `any` has no native representation; give this value a
concrete type
src/greet.lucent.ts:1:23
1 │ export function greet(name: any): string {
│ ^^^^^^^^^
fix use a concrete type, a union, or a generic parameter
docs lucent explain LUCENT2001

lucent build, lucent check, lucent dev and the editor plugin report the same diagnostics. A build with one writes nothing. lucent explain <code> prints the explanation this page shows.

Codes are stable. 1xxx are syntax and built-ins, 2xxx types without a native form, 3xxx modules, platform code and what may run on which thread. 9001 is a TypeScript error: Lucent compiles only programs that type-check.

CodeMeaning
LUCENT1001Syntax outside the subset, such as var, getters in object literals, or an async generator.
LUCENT1002An operator the subset doesn't support, such as delete, in on anything but a record, or instanceof with a generic class. A loose == that JavaScript converts for, or comparing two functions, is refused too.
LUCENT1003A built-in function or method Lucent does not implement, such as Symbol(), eval or an unknown Math, Number or string method.
LUCENT1004A destructuring form outside the subset: object rest, computed keys, or a pattern without an initializer.
LUCENT1005A class feature outside the subset, such as extending a built-in other than Error, or an override that changes the native signature.
LUCENT1006Throwing a value that is not an Error.
LUCENT1007A call Lucent cannot compile, such as spread arguments outside rest parameters, or a platform class without a constructor binding.
LUCENT1008An assignment to something that cannot be assigned, such as an unknown field or a function.
LUCENT1009A loop over a value that is not iterable in Lucent, or for await.
LUCENT1010await on a platform SDK object, such as a Play services Task or a Java future: it is not a promise.
LUCENT2001any, or unknown outside a catch clause: every value needs a native type.
LUCENT2002A type with no native representation: intersections, symbol, object, WeakMap, Intl, or an index signature mixed with properties.
LUCENT2003An object used as a type with a different shape; object types must match exactly to share a native representation.
LUCENT2004A collection used where its element type would change (A[] as (A | B)[]); annotate the value with the target type.
LUCENT2005A union JavaScript values cannot be told apart by at the boundary; add a string-literal discriminant.
LUCENT2006A value that cannot cross the JavaScript boundary, such as a generator, a match result, an AbortSignal or a platform object.
LUCENT2007A generic function or class exported to JavaScript; export a concrete wrapper.
LUCENT2008A value used as an interface its class does not declare with implements.
LUCENT2009A class member whose native signature differs from the interface member it implements.
LUCENT3001An import from something other than another *.lucent.ts file or a built-in lucent: module.
LUCENT3002A top-level statement that is not a declaration.
LUCENT3003An export form Lucent does not support: export lists, re-exports, default exports.
LUCENT3004A platform SDK import in a file whose platform cannot use it, an SDK module without bindings, or platform code used outside its platform.
LUCENT3005Platform implementations that do not match their shared declaration file.
LUCENT3006A main-thread-only platform API used outside main(() => …).
LUCENT3007A platform API newer than the oldest supported OS version, used without an available() or SDK_INT check around it.
LUCENT3008An Android SDK argument that is a constant outside the @IntDef or @StringDef group the SDK allows there.
LUCENT3009An Android SDK member marked @WorkerThread, called inside main(() => …) or a main-thread callback.
LUCENT3010Two Lucent modules in one app whose files give them the same module name.
LUCENT3011A compute() task touches module state or a computed constant, native code unfit for workers, code the compiler cannot follow, or asynchronous work.
LUCENT3012A compute() task's input or result holds something other than data, such as a function, a promise or a main-thread native object.
LUCENT3020An exported .lucent.tsx function that returns a view but cannot be a component, or Lucent code calling a component.
LUCENT3021A component prop, event or ref command whose type views cannot carry, or whose name React or the host keeps.
LUCENT3022A component's setup, or a function it creates, using module state, code the compiler cannot follow, or native code unfit for the main thread.
LUCENT3023A component whose props, events or commands differ between platforms or from its declaration, or that is a component on one platform only.
LUCENT3024A component's SwiftUI or Jetpack Compose body that Lucent cannot write out in Swift or Kotlin, or toolkit code used outside such a body.
LUCENT3025JSX of UIKit or Android views that Lucent cannot make: a view, attribute or child its declarations do not provide for.
LUCENT3030A span that withRead or withWrite lends escapes its callback: returned, stored, kept by a closure or callee, or held across await.
LUCENT3031A NativeBuffer is used after transfer() or after it was handed to compute(), which move its bytes to another buffer.
LUCENT9001A TypeScript error. Lucent stops at type errors, because it compiles from the checker's types.

Syntax outside the subset

Lucent compiles a subset of TypeScript whose every construct has a native equivalent with the same behaviour. var (function scoping and hoisting), with, labels on blocks, accessors in object literals and async generators fall outside it.

Fix: rewrite it with the supported form: let/const for var, a class for an object with accessors.

example.lucent.ts (wrong)
export function total(xs: number[]): number {
var sum = 0;
for (const x of xs) sum += x;
return sum;
}

Unsupported operator

Objects in Lucent have a fixed native layout, so an operator that adds or removes properties at run time (delete) has no native equivalent. in works on records (Record<string, T>), whose keys are dynamic, and not on objects with a known shape. Two functions cannot be compared, because a function value has no stable identity (a named function is a new value at each use).

Fix: use a Map or a Record for keys that come and go, or an optional field for one that may be missing.

example.lucent.ts (wrong)
export function clear(tags: { name?: string }): { name?: string } {
delete tags.name;
return tags;
}

Built-in without a native implementation

Every built-in a module calls runs as native code from the Lucent runtime. The ones listed in the language reference are implemented with JavaScript's exact semantics; the rest are reported rather than approximated.

Fix: use a built-in Lucent implements, or write the helper in the module.

example.lucent.ts (wrong)
export function rotate(xs: number[]): number[] {
return xs.copyWithin(0, 1);
}

Unsupported destructuring

Object rest ({ a, ...rest }) would need a new object type, made of the remaining fields at run time. Computed keys would need a dynamic lookup. Neither has a fixed native layout.

Fix: name the fields you need, or pass the whole object.

example.lucent.ts (wrong)
type User = { id: number; name: string; email: string };
export function contact(u: User): { name: string; email: string } {
const { id, ...rest } = u;
return rest;
}

Unsupported class feature

Classes compile to C++ classes. A subclass of Map or Array would inherit the runtime's container internals. An override whose parameters or result differ from the base method's can't share its native slot.

Fix: hold the built-in in a field instead of extending it, and keep overrides' signatures the same as the base method's.

example.lucent.ts (wrong)
class Counts extends Map<string, number> {}
export function size(): number {
return new Counts().size;
}

Throwing a value that is not an Error

Errors cross into JavaScript as Error objects with a name, a message and a stack that points at the Lucent source. A thrown string or number has none of that and no native type to catch it by.

Fix: throw new Error(…), a built-in error such as TypeError, or a class that extends Error.

example.lucent.ts (wrong)
export function port(text: string): number {
const n = Number(text);
if (!Number.isInteger(n)) throw "not a port";
return n;
}

Call Lucent cannot compile

Native calls pass a fixed number of arguments of known types. Spreading an array into ordinary parameters has no native form. Neither does a platform API called as a promise where it has none, or a platform class without an initializer.

Fix: pass the arguments one by one, or declare the callee with a rest parameter.

example.lucent.ts (wrong)
function add(a: number, b: number): number {
return a + b;
}
export function sum(pair: [number, number]): number {
return add(...pair);
}

Assignment to something that cannot be assigned

Assignments write to a native variable or field. A function, an import, or a field the value's type does not have is not one.

Fix: assign a variable or a field the type declares.

example.lucent.ts (wrong)
function tick(): number {
return 1;
}
export function reset(): void {
// @ts-expect-error: assigning a function
tick = () => 0;
}

Loop over a value that is not iterable

for…of works on arrays, strings, maps, sets, typed arrays and generators, and for…in on records. Async iteration (for await) is not supported: await each promise in an ordinary loop.

Fix: loop over an array (Object.keys, Array.from), or await inside a plain loop.

example.lucent.ts (wrong)
export async function total(xs: Promise<number>[]): Promise<number> {
let sum = 0;
for await (const x of xs) sum += x;
return sum;
}

Await on a native object

A native object reports its completion through its own API, usually a listener, so awaiting it would give the object back at once. Lucent does not recognize libraries by class name, so it cannot pick that API for you. fromCallback from lucent:core turns the listener into a promise: register it, then call resolve or reject from it.

Fix: wrap the listener or callback that reports its completion in fromCallback (lucent:core), and await that.

example.lucent.ts (wrong)
import { PLATFORM } from "lucent:platform";
import { CompletableFuture } from "lucent:android/java.util.concurrent";
export async function ready(): Promise<boolean> {
if (PLATFORM !== "android") return true;
const value = await CompletableFuture.completedFuture("ready");
return value !== null;
}

Value without a native type

Native code needs to know each value's layout at compile time. any gives it none, and unknown only stands for a caught error, whose type JavaScript does not say.

Fix: use a concrete type, a union, or a generic parameter.

example.lucent.ts (wrong)
export function size(value: any): number {
return value.length;
}

Type without a native representation

Each type maps to one native representation. An intersection can combine unrelated layouts, symbol is not implemented yet, and object says nothing about the layout. A native member whose types cannot cross yet is named with its symbol and artifact: wrap it in Swift or Kotlin of your own.

Fix: spell the combined type out as one object type, or use a concrete type.

example.lucent.ts (wrong)
type Named = { name: string };
type Aged = { age: number };
export function label(p: Named & Aged): string {
return `${p.name} (${p.age})`;
}

Object used as a type with a different shape

Object types with the same fields share one native struct. TypeScript lets a value with more fields stand for a type with fewer. Natively they are different structs, and the extra fields would have nowhere to go.

Fix: pass exactly the fields the type declares, or widen the parameter's type.

example.lucent.ts (wrong)
type Point = { x: number; y: number };
type Point3 = { x: number; y: number; z: number };
function norm(p: Point): number {
return Math.hypot(p.x, p.y);
}
export function flat(p: Point3): number {
return norm(p);
}

Collection used with another element type

An array of numbers and an array of number | string have different native element types. One can't be used as the other: writes through the wider type wouldn't fit.

Fix: annotate the collection with the element type it is used as.

example.lucent.ts (wrong)
function first(xs: (number | string)[]): string {
return String(xs[0]);
}
export function f(): string {
const xs = [1, 2];
return first(xs);
}

Union JavaScript values cannot be told apart by

A value from JavaScript carries no type, so Lucent decides which union member it is from the value itself. Object members need a string-literal field, such as kind, whose value names the member.

Fix: add a string-literal field such as kind: "circle" to each object member.

example.lucent.ts (wrong)
export function area(shape: { radius: number } | { side: number }): number {
return "radius" in shape ? Math.PI * shape.radius ** 2 : shape.side ** 2;
}

Value that cannot cross the JavaScript boundary

Exports convert their parameters and results between JavaScript and native values. Some native values have no JavaScript form: a generator's state, a platform object, a signal made in native code.

Fix: return plain data (an array instead of a generator, fields instead of a platform object).

example.lucent.ts (wrong)
export function* count(n: number): Generator<number> {
for (let i = 0; i < n; i++) yield i;
}

Generic export

Generics compile to C++ templates, instantiated for the types the module uses. JavaScript calls an export without types, so there is no instantiation to call.

Fix: keep the generic function private and export concrete wrappers.

example.lucent.ts (wrong)
export function last<T>(items: T[]): T | undefined {
return items[items.length - 1];
}

Interface not declared with implements

An interface with methods compiles to an abstract C++ base class, and only classes that declare implements derive from it. A class that matches the interface only by shape, or an object literal, is not one of them.

Fix: add implements to the class.

example.lucent.ts (wrong)
interface Shape {
area(): number;
}
class Square {
constructor(readonly side: number) {}
area(): number {
return this.side ** 2;
}
}
export function area(): number {
const s: Shape = new Square(2);
return s.area();
}

Method signature differs from the interface's

The implementing method overrides the interface's native method, so it needs the same parameter and result types. TypeScript accepts compatible variations (an optional parameter, a narrower result) that are different native signatures.

Fix: declare the method with exactly the interface's parameter and result types.

example.lucent.ts (wrong)
interface Store {
get(key: string): string | undefined;
}
class Memory implements Store {
get(key: string): string {
return key;
}
}
export function read(): string | undefined {
const s: Store = new Memory();
return s.get("a");
}

Import from outside Lucent

Everything a module runs is compiled to native code, so it can only use other Lucent modules and the built-in lucent: modules (lucent:core, lucent:thread, lucent:platform, the SDKs). An npm package or a plain .ts file has no native implementation.

Fix: move the code into a *.lucent.ts module, or use a lucent: module.

clock.ts (wrong)
export function now(): number {
return Date.now();
}

Statement at the top level

A module's top level holds declarations only: functions, classes, types and variables. Statements that run when the module loads have no native place to run, since native modules are created on first use.

Fix: move the statement into a function, or into a variable's initializer.

example.lucent.ts (wrong)
const cache = new Map<string, number>();
cache.set("zero", 0);
export function lookup(k: string): number | undefined {
return cache.get(k);
}

Unsupported export form

Each export becomes a property of the module's native object, named by its declaration. Export lists, re-exports and default exports name exports apart from their declarations.

Fix: put export on the declaration itself.

example.lucent.ts (wrong)
function twice(n: number): number {
return n * 2;
}
export { twice };

Platform SDK import where it cannot be used

lucent:ios/* modules exist on iOS and lucent:android/* on Android. A shared module uses them inside if (PLATFORM === "ios") (or the Android branch), and a *.ios.lucent.ts file only its own platform's.

Fix: use the SDK inside a PLATFORM branch, or in the platform's own file.

example.lucent.ts (wrong)
import { UIDevice } from "lucent:ios/UIKit";
import { main } from "lucent:thread";
export async function model(): Promise<string> {
return main(() => UIDevice.current.model);
}

Platform files that do not match their declaration

A module split into name.ios.lucent.ts and name.android.lucent.ts declares its exports in name.lucent.ts. Every platform file must export exactly the declared functions, with compatible types, so JavaScript sees one module.

Fix: export the same functions, with the declared types, from every platform file.

haptics.lucent.ts (wrong)
export declare function tap(): Promise<void>;

Main-thread API off the main thread

Lucent code runs on its own thread. UIKit, and other APIs the SDK marks main-thread only, must be called on the main thread. main(() => …) from lucent:thread runs a function there, and resolves with its result.

Fix: wrap the call in main(() => …) from lucent:thread.

example.lucent.ts (wrong)
import { PLATFORM } from "lucent:platform";
import { UIDevice } from "lucent:ios/UIKit";
export async function model(): Promise<string> {
if (PLATFORM === "ios") return UIDevice.current.model;
return "unknown";
}

Platform API newer than the oldest supported OS

Apps run on older OS versions than the SDK they build with. An API introduced later crashes there, so Lucent requires a check that the running OS has it.

Fix: check first: if (available("ios", 16)) …, if (available("android", 26)) … or Build_VERSION.SDK_INT >= 26.

example.lucent.ts (wrong)
import { PLATFORM } from "lucent:platform";
import { VibrationEffect } from "lucent:android/android.os";
export async function effect(): Promise<string> {
if (PLATFORM === "android") {
VibrationEffect.createOneShot(10n, 10);
return "made";
}
return "none";
}

Constant outside its group

The SDK says which constants a parameter takes (@IntDef, @StringDef). A literal or constant outside that group compiles, as in Java, but the platform rejects or misreads it; Lucent warns, as Android lint does.

Fix: pass one of the constants the warning lists.

example.lucent.ts (wrong)
import { PLATFORM } from "lucent:platform";
import { Toast } from "lucent:android/android.widget";
import { appContext } from "lucent:android";
export async function toast(): Promise<boolean> {
if (PLATFORM === "android") return Toast.makeText(appContext(), "hi", 5) !== null;
return false;
}

Blocking call on the main thread

@WorkerThread members do blocking work (disk, network, IPC). On the main thread they freeze the UI and can trigger an "application not responding" dialog. The call compiles, but Lucent warns, as Android lint does.

Fix: call it outside main(() => …): async Lucent functions run on the Lucent thread.

example.lucent.ts (wrong)
import { PLATFORM } from "lucent:platform";
import { BlockedNumberContract } from "lucent:android/android.provider";
import { appContext } from "lucent:android";
import { main } from "lucent:thread";
export async function blocked(n: string): Promise<boolean> {
if (PLATFORM === "android") return main(() => BlockedNumberContract.isBlocked(appContext(), n));
return false;
}

Two modules with one name

A module's name is its file name without the directory, the platform suffix and .lucent.ts (inside a Lucent package, its path under the package's sources). The app's native side registers each module under that name, so two files that share it would be one module.

Fix: rename one of the files.

screens/clock.lucent.ts (wrong)
export function now(): number {
return Date.now();
}

Code that cannot run in a compute task

A compute task runs on a worker thread while module code keeps running, so it may only touch its input and what it makes. Module state, non-literal constants (a reload assigns them again), main-thread native code, function values the compiler cannot follow, await and timers break that rule. The message gives the path, through every call, to what breaks it.

Fix: pass what the task needs as its input, and apply its result where you await it.

example.lucent.ts (wrong)
import { compute } from "lucent:core";
const weights: number[] = [1, 2, 3];
function score(x: number): number {
return x * (weights[0] ?? 1);
}
export async function run(x: number): Promise<number> {
return await compute(score, x);
}

Value that cannot cross to a compute task

A compute task's input is copied at submission, and its result comes back to the caller. Both must be numbers, strings, booleans, arrays, tuples, records, maps, sets, byte arrays, dates, or objects made of those. Functions, promises, main-thread native objects and values of unknown or generic type cannot cross, and the message names which part.

Fix: pass the data the task needs, and keep functions, signals and native objects on the calling side.

example.lucent.ts (wrong)
import { compute } from "lucent:core";
type Job = { values: number[]; report: (n: number) => void };
function total(job: Job): number {
return job.values.reduce((a, b) => a + b, 0);
}
export async function run(values: number[]): Promise<number> {
return await compute(total, { values, report: () => {} });
}

Export that cannot be a component

An exported .lucent.tsx function that returns a platform view (a UIView, an Android View, or a subclass) is a component. React mounts it through a native host, and Lucent identifies it by its package's name, module path and export name. So it returns its view on every path and synchronously, takes one props object or none, and has no type parameters.

Fix: return the view on every path, from one props object, and move other work into a separate function.

package.json (wrong)
{ "name": "example-app" }

Component prop, event or command that cannot cross

Props, event arguments and command arguments and results are plain data: numbers, strings, booleans, string literal unions, arrays and plain objects, possibly null. A function prop is an event named on…, whose call from the view posts its arguments to JavaScript, so it returns nothing. key, ref, children and style belong to React and the host.

Fix: pass plain data, make events return nothing, and name them onSomething.

package.json (wrong)
{ "name": "example-app" }

Component code that cannot run on the main thread

A component sets up its view on the main (UI) thread, where the handlers it gives the view run too. Module variables belong to the module's thread, code the compiler cannot follow may do anything, and blocking or background-only native code freezes the UI. Calling an event prop directly (props.onChange?.(value)) is fine: it posts the event to JavaScript.

Fix: keep the state in the component (a local), or pass it in as a prop.

package.json (wrong)
{ "name": "example-app" }

Component that differs between platforms

React sees one component on every platform, so its props, events and commands are the same in every platform's implementation. A split module's shared declaration states the same contract, and the component returns a view on every platform.

Fix: give every platform's implementation the declared props.

package.json (wrong)
{ "name": "example-app" }

SwiftUI or Compose body that cannot be compiled

A component can draw with its platform's toolkit, SwiftUI or Jetpack Compose (internal, under LUCENT_VIEWS=fabric). Lucent writes that body out as Swift or Kotlin, showing the numbers, booleans and strings its setup computes. Toolkit views exist only in a body, whose callbacks call the setup's functions: it doesn't change the setup's state or send events.

Fix: make the view in the body, and move logic into a function of the setup that the body calls.

package.json (wrong)
{ "name": "example-app" }

Native view JSX that cannot be compiled

A component can return its platform's views as JSX, declared as returning UIView or View (internal, under LUCENT_VIEWS=fabric). A tag takes what its class's declarations provide: writable properties, setters, control or listener events, and children where it inserts views at an index. Attributes are kept up to date like effects, and a child may come and go: {cond && <X />}, {c ? <X /> : <Y />}, or a keyed list, {items.map((item) => <X key={item.id} />)}.

Fix: write each attribute on its element, and set what the declarations do not provide for in setup code.

package.json (wrong)
{ "name": "example-app" }

Borrowed bytes that outlive their borrow

withRead and withWrite lend a buffer's bytes for one synchronous call, and refuse conflicting use only while it runs. After it, a task may write, release or move the bytes, so the message shows how the span would outlive the call. Copy what you need inside the callback, or snapshot the buffer with toUint8Array().

Fix: copy what you need out of the span inside the callback, and return that.

example.lucent.ts (wrong)
import { NativeBuffer } from "lucent:core";
export function first(buffer: NativeBuffer): number {
const bytes = buffer.withRead((lent) => lent);
return bytes[0] ?? 0;
}

Native buffer used after it moved

Moving a buffer hands its bytes, uncopied, to a new owner: the buffer that transfer returns, or a compute task. The old buffer then throws an InvalidStateError on every use, and where the move certainly came first, the compiler reports the use. Use the buffer the move gave you, or copy the bytes before moving them.

Fix: use the buffer transfer() returned, or what the task gives back.

example.lucent.ts (wrong)
import { NativeBuffer } from "lucent:core";
export function size(): number {
const buffer = NativeBuffer.allocate(8);
const moved = buffer.transfer();
return buffer.byteLength + moved.byteLength;
}

TypeScript error

Lucent compiles only programs that type-check: every value's native type comes from the TypeScript checker. The message is TypeScript's own. When a native library's type lacks a member, Lucent adds which module and installed version declare it, or that its iOS module needs importing.

Fix: fix the type error; your editor shows the same message.

example.lucent.ts (wrong)
export function double(n: number): number {
return n + "";
}