Diagnostics
Every LUCENT code, with its reason, its fix, and a wrong and a right example.
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 LUCENT2001lucent 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.
| Code | Meaning |
|---|---|
LUCENT1001 | Syntax outside the subset, such as var, getters in object literals, or an async generator. |
LUCENT1002 | An 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. |
LUCENT1003 | A built-in function or method Lucent does not implement, such as Symbol(), eval or an unknown Math, Number or string method. |
LUCENT1004 | A destructuring form outside the subset: object rest, computed keys, or a pattern without an initializer. |
LUCENT1005 | A class feature outside the subset, such as extending a built-in other than Error, or an override that changes the native signature. |
LUCENT1006 | Throwing a value that is not an Error. |
LUCENT1007 | A call Lucent cannot compile, such as spread arguments outside rest parameters, or a platform class without a constructor binding. |
LUCENT1008 | An assignment to something that cannot be assigned, such as an unknown field or a function. |
LUCENT1009 | A loop over a value that is not iterable in Lucent, or for await. |
LUCENT1010 | await on a platform SDK object, such as a Play services Task or a Java future: it is not a promise. |
LUCENT2001 | any, or unknown outside a catch clause: every value needs a native type. |
LUCENT2002 | A type with no native representation: intersections, symbol, object, WeakMap, Intl, or an index signature mixed with properties. |
LUCENT2003 | An object used as a type with a different shape; object types must match exactly to share a native representation. |
LUCENT2004 | A collection used where its element type would change (A[] as (A | B)[]); annotate the value with the target type. |
LUCENT2005 | A union JavaScript values cannot be told apart by at the boundary; add a string-literal discriminant. |
LUCENT2006 | A value that cannot cross the JavaScript boundary, such as a generator, a match result, an AbortSignal or a platform object. |
LUCENT2007 | A generic function or class exported to JavaScript; export a concrete wrapper. |
LUCENT2008 | A value used as an interface its class does not declare with implements. |
LUCENT2009 | A class member whose native signature differs from the interface member it implements. |
LUCENT3001 | An import from something other than another *.lucent.ts file or a built-in lucent: module. |
LUCENT3002 | A top-level statement that is not a declaration. |
LUCENT3003 | An export form Lucent does not support: export lists, re-exports, default exports. |
LUCENT3004 | A platform SDK import in a file whose platform cannot use it, an SDK module without bindings, or platform code used outside its platform. |
LUCENT3005 | Platform implementations that do not match their shared declaration file. |
LUCENT3006 | A main-thread-only platform API used outside main(() => …). |
LUCENT3007 | A platform API newer than the oldest supported OS version, used without an available() or SDK_INT check around it. |
LUCENT3008 | An Android SDK argument that is a constant outside the @IntDef or @StringDef group the SDK allows there. |
LUCENT3009 | An Android SDK member marked @WorkerThread, called inside main(() => …) or a main-thread callback. |
LUCENT3010 | Two Lucent modules in one app whose files give them the same module name. |
LUCENT3011 | A compute() task touches module state or a computed constant, native code unfit for workers, code the compiler cannot follow, or asynchronous work. |
LUCENT3012 | A compute() task's input or result holds something other than data, such as a function, a promise or a main-thread native object. |
LUCENT3020 | An exported .lucent.tsx function that returns a view but cannot be a component, or Lucent code calling a component. |
LUCENT3021 | A component prop, event or ref command whose type views cannot carry, or whose name React or the host keeps. |
LUCENT3022 | A component's setup, or a function it creates, using module state, code the compiler cannot follow, or native code unfit for the main thread. |
LUCENT3023 | A component whose props, events or commands differ between platforms or from its declaration, or that is a component on one platform only. |
LUCENT3024 | A component's SwiftUI or Jetpack Compose body that Lucent cannot write out in Swift or Kotlin, or toolkit code used outside such a body. |
LUCENT3025 | JSX of UIKit or Android views that Lucent cannot make: a view, attribute or child its declarations do not provide for. |
LUCENT3030 | A span that withRead or withWrite lends escapes its callback: returned, stored, kept by a closure or callee, or held across await. |
LUCENT3031 | A NativeBuffer is used after transfer() or after it was handed to compute(), which move its bytes to another buffer. |
LUCENT9001 | A TypeScript error. Lucent stops at type errors, because it compiles from the checker's types. |
LUCENT1001
Section titled “LUCENT1001”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.
export function total(xs: number[]): number { var sum = 0; for (const x of xs) sum += x; return sum;}export function total(xs: number[]): number { let sum = 0; for (const x of xs) sum += x; return sum;}LUCENT1002
Section titled “LUCENT1002”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.
export function clear(tags: { name?: string }): { name?: string } { delete tags.name; return tags;}export function clear(tags: { name?: string }): { name?: string } { tags.name = undefined; return tags;}LUCENT1003
Section titled “LUCENT1003”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.
export function rotate(xs: number[]): number[] { return xs.copyWithin(0, 1);}export function rotate(xs: number[]): number[] { return [...xs.slice(1), ...xs.slice(0, 1)];}LUCENT1004
Section titled “LUCENT1004”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.
type User = { id: number; name: string; email: string };export function contact(u: User): { name: string; email: string } { const { id, ...rest } = u; return rest;}type User = { id: number; name: string; email: string };export function contact(u: User): { name: string; email: string } { const { name, email } = u; return { name, email };}LUCENT1005
Section titled “LUCENT1005”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.
class Counts extends Map<string, number> {}export function size(): number { return new Counts().size;}class Counts { readonly map = new Map<string, number>();}export function size(): number { return new Counts().map.size;}LUCENT1006
Section titled “LUCENT1006”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.
export function port(text: string): number { const n = Number(text); if (!Number.isInteger(n)) throw "not a port"; return n;}export function port(text: string): number { const n = Number(text); if (!Number.isInteger(n)) throw new RangeError(`not a port: ${text}`); return n;}LUCENT1007
Section titled “LUCENT1007”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.
function add(a: number, b: number): number { return a + b;}export function sum(pair: [number, number]): number { return add(...pair);}function add(a: number, b: number): number { return a + b;}export function sum(pair: [number, number]): number { return add(pair[0], pair[1]);}LUCENT1008
Section titled “LUCENT1008”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.
function tick(): number { return 1;}export function reset(): void { // @ts-expect-error: assigning a function tick = () => 0;}let tick = (): number => 1;export function reset(): void { tick = () => 0;}export function now(): number { return tick();}LUCENT1009
Section titled “LUCENT1009”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.
export async function total(xs: Promise<number>[]): Promise<number> { let sum = 0; for await (const x of xs) sum += x; return sum;}export async function total(xs: Promise<number>[]): Promise<number> { let sum = 0; for (const x of xs) sum += await x; return sum;}LUCENT1010
Section titled “LUCENT1010”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.
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;}import { fromCallback } from "lucent:core";import { PLATFORM } from "lucent:platform";import type { Throwable } from "lucent:android/java.lang";import { CompletableFuture } from "lucent:android/java.util.concurrent";export async function ready(): Promise<boolean> { if (PLATFORM !== "android") return true; const future = CompletableFuture.completedFuture("ready"); const value = await fromCallback<string | null>((resolve, reject) => { future?.whenComplete((v: string | null, failure: Throwable | null) => { if (failure) reject(new Error(failure.getMessage() ?? "failed")); else resolve(v); }); }); return value !== null;}LUCENT2001
Section titled “LUCENT2001”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.
export function size(value: any): number { return value.length;}export function size(value: string): number { return value.length;}LUCENT2002
Section titled “LUCENT2002”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.
type Named = { name: string };type Aged = { age: number };export function label(p: Named & Aged): string { return `${p.name} (${p.age})`;}type Person = { name: string; age: number };export function label(p: Person): string { return `${p.name} (${p.age})`;}LUCENT2003
Section titled “LUCENT2003”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.
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);}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({ x: p.x, y: p.y });}LUCENT2004
Section titled “LUCENT2004”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.
function first(xs: (number | string)[]): string { return String(xs[0]);}export function f(): string { const xs = [1, 2]; return first(xs);}function first(xs: (number | string)[]): string { return String(xs[0]);}export function f(): string { const xs: (number | string)[] = [1, 2]; return first(xs);}LUCENT2005
Section titled “LUCENT2005”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.
export function area(shape: { radius: number } | { side: number }): number { return "radius" in shape ? Math.PI * shape.radius ** 2 : shape.side ** 2;}export function area( shape: { kind: "circle"; radius: number } | { kind: "square"; side: number },): number { return shape.kind === "circle" ? Math.PI * shape.radius ** 2 : shape.side ** 2;}LUCENT2006
Section titled “LUCENT2006”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).
export function* count(n: number): Generator<number> { for (let i = 0; i < n; i++) yield i;}function* count(n: number): Generator<number> { for (let i = 0; i < n; i++) yield i;}export function counted(n: number): number[] { return [...count(n)];}LUCENT2007
Section titled “LUCENT2007”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.
export function last<T>(items: T[]): T | undefined { return items[items.length - 1];}function last<T>(items: T[]): T | undefined { return items[items.length - 1];}export function lastName(names: string[]): string | undefined { return last(names);}LUCENT2008
Section titled “LUCENT2008”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.
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();}interface Shape { area(): number;}class Square implements Shape { constructor(readonly side: number) {} area(): number { return this.side ** 2; }}export function area(): number { const s: Shape = new Square(2); return s.area();}LUCENT2009
Section titled “LUCENT2009”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.
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");}interface Store { get(key: string): string | undefined;}class Memory implements Store { get(key: string): string | undefined { return key; }}export function read(): string | undefined { const s: Store = new Memory(); return s.get("a");}LUCENT3001
Section titled “LUCENT3001”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.
export function now(): number { return Date.now();}import { now } from "./clock";export function stamp(): number { return now();}import { now } from "lucent:core";export function stamp(): number { return now();}LUCENT3002
Section titled “LUCENT3002”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.
const cache = new Map<string, number>();cache.set("zero", 0);export function lookup(k: string): number | undefined { return cache.get(k);}const cache = new Map<string, number>([["zero", 0]]);export function lookup(k: string): number | undefined { return cache.get(k);}LUCENT3003
Section titled “LUCENT3003”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.
function twice(n: number): number { return n * 2;}export { twice };export function twice(n: number): number { return n * 2;}LUCENT3004
Section titled “LUCENT3004”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.
import { UIDevice } from "lucent:ios/UIKit";import { main } from "lucent:thread";export async function model(): Promise<string> { return main(() => UIDevice.current.model);}import { PLATFORM } from "lucent:platform";import { UIDevice } from "lucent:ios/UIKit";import { main } from "lucent:thread";export async function model(): Promise<string> { if (PLATFORM === "ios") return main(() => UIDevice.current.model); return "unknown";}LUCENT3005
Section titled “LUCENT3005”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.
export declare function tap(): Promise<void>;export async function tap(strength: number): Promise<void> {}export async function tap(): Promise<void> {}export declare function tap(): Promise<void>;export async function tap(): Promise<void> {}export async function tap(): Promise<void> {}LUCENT3006
Section titled “LUCENT3006”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.
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";}import { PLATFORM } from "lucent:platform";import { UIDevice } from "lucent:ios/UIKit";import { main } from "lucent:thread";export async function model(): Promise<string> { if (PLATFORM === "ios") return main(() => UIDevice.current.model); return "unknown";}LUCENT3007
Section titled “LUCENT3007”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.
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";}import { PLATFORM } from "lucent:platform";import { available } from "lucent:android";import { VibrationEffect } from "lucent:android/android.os";export async function effect(): Promise<string> { if (PLATFORM === "android") { if (!available("android", 26)) return "too old"; VibrationEffect.createOneShot(10n, 10); return "made"; } return "none";}LUCENT3008
Section titled “LUCENT3008”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.
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;}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", Toast.LENGTH_SHORT) !== null; return false;}LUCENT3009
Section titled “LUCENT3009”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.
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;}import { PLATFORM } from "lucent:platform";import { BlockedNumberContract } from "lucent:android/android.provider";import { appContext } from "lucent:android";export async function blocked(n: string): Promise<boolean> { if (PLATFORM === "android") return BlockedNumberContract.isBlocked(appContext(), n); return false;}LUCENT3010
Section titled “LUCENT3010”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.
export function now(): number { return Date.now();}export function zone(): string { return "UTC";}export function now(): number { return Date.now();}export function zone(): string { return "UTC";}LUCENT3011
Section titled “LUCENT3011”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.
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);}import { compute } from "lucent:core";const weights: number[] = [1, 2, 3];function score(job: { x: number; weights: number[] }): number { return job.x * (job.weights[0] ?? 1);}export async function run(x: number): Promise<number> { return await compute(score, { x, weights });}LUCENT3012
Section titled “LUCENT3012”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.
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: () => {} });}import { compute } from "lucent:core";function total(values: number[]): number { return values.reduce((a, b) => a + b, 0);}export async function run(values: number[]): Promise<number> { return await compute(total, values);}LUCENT3020
Section titled “LUCENT3020”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.
{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string; shown: boolean }) { if (!props.shown) return undefined; if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string }) { if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}LUCENT3021
Section titled “LUCENT3021”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.
{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string; onMeasure?: (width: number) => number }) { if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string; onMeasure?: (width: number) => void }) { if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}LUCENT3022
Section titled “LUCENT3022”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.
{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";let shown = 0;export function Title(props: { title: string }) { shown++; if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}{ "name": "example-app" }import { PLATFORM } from "lucent:platform";import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string }) { if (PLATFORM === "ios") { const label = new UILabel(); label.text = props.title; return label; } const text = new TextView(appContext()); text.setText(props.title); return text;}LUCENT3023
Section titled “LUCENT3023”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.
{ "name": "example-app" }import type { TextView } from "lucent:android/android.widget";import type { UILabel } from "lucent:ios/UIKit";export declare function Title(props: { title: string; lines: number }): UILabel | TextView;import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string }): UILabel { const label = new UILabel(); label.text = props.title; return label;}import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";export function Title(props: { title: string }): TextView { const text = new TextView(appContext()); text.setText(props.title); return text;}{ "name": "example-app" }import type { TextView } from "lucent:android/android.widget";import type { UILabel } from "lucent:ios/UIKit";export declare function Title(props: { title: string; lines: number }): UILabel | TextView;import { UILabel } from "lucent:ios/UIKit";export function Title(props: { title: string; lines: number }): UILabel { const label = new UILabel(); label.text = props.title; return label;}import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";export function Title(props: { title: string; lines: number }): TextView { const text = new TextView(appContext()); text.setText(props.title); return text;}LUCENT3024
Section titled “LUCENT3024”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.
{ "name": "example-app" }import type { TextView } from "lucent:android/android.widget";import type { View } from "lucent:swiftui";export declare function Title(props: { title: string }): View | TextView;import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { effect } from "lucent:ui";export function Title(props: { title: string }): TextView { const text = new TextView(appContext()); effect(() => text.setText(props.title)); return text;}import { Text } from "lucent:swiftui";import { signal } from "lucent:ui";export function Title(props: { title: string }) { const taps = signal(0); return <Text onTapGesture={() => taps.set(taps.peek() + 1)}>{props.title}</Text>;}{ "name": "example-app" }import type { TextView } from "lucent:android/android.widget";import type { View } from "lucent:swiftui";export declare function Title(props: { title: string }): View | TextView;import { appContext } from "lucent:android";import { TextView } from "lucent:android/android.widget";import { effect } from "lucent:ui";export function Title(props: { title: string }): TextView { const text = new TextView(appContext()); effect(() => text.setText(props.title)); return text;}import { Text } from "lucent:swiftui";import { signal } from "lucent:ui";export function Title(props: { title: string }) { const taps = signal(0); const tap = () => { taps.set(taps.peek() + 1); }; return <Text onTapGesture={() => tap()}>{props.title}</Text>;}LUCENT3025
Section titled “LUCENT3025”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.
{ "name": "example-app" }import type { View } from "lucent:android/android.view";import type { UIView } from "lucent:ios/UIKit";export declare function Title(props: { title: string }): UIView | View;import type { View } from "lucent:android/android.view";import { TextView } from "lucent:android/android.widget";export function Title(props: { title: string }): View { return <TextView text={props.title} />;}import { UILabel, type UIView } from "lucent:ios/UIKit";export function Title(props: { title: string }): UIView { const attributes = { text: props.title }; return <UILabel {...attributes} />;}{ "name": "example-app" }import type { View } from "lucent:android/android.view";import type { UIView } from "lucent:ios/UIKit";export declare function Title(props: { title: string }): UIView | View;import type { View } from "lucent:android/android.view";import { TextView } from "lucent:android/android.widget";export function Title(props: { title: string }): View { return <TextView text={props.title} />;}import { UILabel, type UIView } from "lucent:ios/UIKit";export function Title(props: { title: string }): UIView { return <UILabel text={props.title} />;}LUCENT3030
Section titled “LUCENT3030”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.
import { NativeBuffer } from "lucent:core";export function first(buffer: NativeBuffer): number { const bytes = buffer.withRead((lent) => lent); return bytes[0] ?? 0;}import { NativeBuffer } from "lucent:core";export function first(buffer: NativeBuffer): number { return buffer.withRead((bytes) => bytes[0] ?? 0);}LUCENT3031
Section titled “LUCENT3031”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.
import { NativeBuffer } from "lucent:core";export function size(): number { const buffer = NativeBuffer.allocate(8); const moved = buffer.transfer(); return buffer.byteLength + moved.byteLength;}import { NativeBuffer } from "lucent:core";export function size(): number { const buffer = NativeBuffer.allocate(8); const moved = buffer.transfer(); return moved.byteLength;}LUCENT9001
Section titled “LUCENT9001”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.
export function double(n: number): number { return n + "";}export function double(n: number): number { return n * 2;}