Functions
KERN has a rich function system: standalone functions, class methods, React hooks, route handlers, guards, and more. All compile to idiomatic TypeScript.
Standalone functions
The fn node declares a function with typed parameters and return type:
fn name=createPlan params="action:PlanAction,ws:WorkspaceSnapshot" returns=Plan
handler <<<
return { id: crypto.randomUUID(), action, workspace: ws, state: 'draft' };
>>>Compiles to:
export function createPlan(
action: PlanAction,
ws: WorkspaceSnapshot,
): Plan {
return { id: crypto.randomUUID(), action, workspace: ws, state: 'draft' };
}Parameters
Parameter syntax is name:Type or name:Type=default, comma-separated:
fn name=search params="query:string,limit:number=20,offset:number=0" returns="SearchResult[]"
handler <<<
return db.search(query, { limit, offset });
>>>Complex types work too — the parser is depth-aware for generics, arrow functions, and nested brackets:
params="fetcher:(key:string)=>Promise<Data>,cache:Record<string,Data>"Async functions
Add async=true to generate an async function:
fn name=fetchUser params="id:string" returns="Promise<User>" async=true
handler <<<
const res = await fetch(`/api/users/${id}`);
return res.json();
>>>Streaming functions
Add stream=true to generate an async generator:
fn name=streamChunks params="prompt:string" returns=StreamChunk async=true stream=true
handler <<<
for await (const chunk of client.stream(prompt)) {
yield chunk;
}
>>>Compiles to:
export async function* streamChunks(
prompt: string,
): AsyncGenerator<StreamChunk> {
for await (const chunk of client.stream(prompt)) {
yield chunk;
}
}Signal & cleanup
The signal child creates an AbortController. The cleanup child wraps the handler in try/finally:
fn name=handleChat params="input:string" returns="Promise<void>" async=true
signal name=abort
handler <<<
const res = await fetch('/api/chat', {
signal: abort.signal, body: input,
});
return res.json();
>>>
cleanup <<<
abort.abort();
>>>Compiles to:
export async function handleChat(input: string): Promise<void> {
const abort = new AbortController();
try {
const res = await fetch('/api/chat', {
signal: abort.signal, body: input,
});
return res.json();
} finally {
abort.abort();
}
}Services (classes)
The service node generates a TypeScript class with fields, constructor, and methods:
service name=TokenTracker
field name=entries type="TokenUsage[]" default="[]" private=true readonly=true
method name=record params="usage:TokenUsage" returns=void
handler <<<
this.entries.push(usage);
>>>
method name=getStats returns=SessionStats
handler <<<
const total = this.entries.reduce((sum, e) => sum + e.tokens, 0);
return { total, count: this.entries.length };
>>>
singleton name=tracker type=TokenTrackerCompiles to:
export class TokenTracker {
private readonly entries: TokenUsage[] = [];
record(usage: TokenUsage): void {
this.entries.push(usage);
}
getStats(): SessionStats {
const total = this.entries.reduce((sum, e) => sum + e.tokens, 0);
return { total, count: this.entries.length };
}
}
export const tracker = new TokenTracker();Method modifiers
private=true— private visibilitystatic=true— static methodasync=true— async methodstream=true— async generator (async *)
Singletons
The singleton child creates a module-level instance: export const tracker = new TokenTracker();
React hooks
The hook node generates a complete React custom hook with state, refs, effects, memos, and callbacks:
hook name=useSearch params="initialQuery:string" returns=UseSearchResult
state name=query type=string init="initialQuery"
ref name=controller type=AbortController init="new AbortController()"
context name=env type=EnvContext source=EnvContext
memo name=cacheKey deps="query"
handler <<<
return buildCacheKey(query);
>>>
callback name=handleFilter params="field:string,value:string" deps="query"
handler <<<
setQuery(prev => updateFilter(prev, field, value));
>>>
effect deps="query"
handler <<<
trackSearch(query);
>>>
returns names="query,handleFilter,cacheKey"Compiles to:
import { useState, useRef, useContext, useMemo, useCallback, useEffect } from 'react';
export function useSearch(initialQuery: string): UseSearchResult {
const [query, setQuery] = useState<string>(initialQuery);
const controller = useRef<AbortController>(new AbortController());
const env = useContext(EnvContext);
const cacheKey = useMemo(() => {
return buildCacheKey(query);
}, [query]);
const handleFilter = useCallback((field: string, value: string) => {
setQuery(prev => updateFilter(prev, field, value));
}, [query]);
useEffect(() => {
trackSearch(query);
}, [query]);
return { query, handleFilter, cacheKey } as const;
}Hook children
| Child | React output | Props |
|---|---|---|
state | useState<T>(init) | name, type, init |
ref | useRef<T>(init) | name, type, init |
context | useContext(source) | name, type, source |
memo | useMemo(() => {...}, deps) | name, deps, handler |
callback | useCallback((p) => {...}, deps) | name, params, deps, handler |
effect | useEffect(() => {...}, deps) | deps, handler, cleanup |
returns | return { ... } as const | names |
Guards
The guard node enforces conditions inside route handlers. If the expression is falsy, it returns an error response:
route GET /api/users/:id
derive user expr={{await db.users.findById(params.id)}}
guard name=exists expr={{user}} else=404
respond 200 json=user
error 404 "Not found"Compiles to Express:
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!(user)) {
return res.status(404).json({ error: 'exists guard failed' });
}
res.json(user);
});Function taxonomy
| Node | Context | Output |
|---|---|---|
fn | Top-level | Standalone function |
method | Inside service | Class method |
constructor | Inside service | Class constructor |
hook | Top-level | React custom hook |
callback | Inside hook | useCallback() |
memo | Inside hook | useMemo() |
effect | Inside hook | useEffect() |
guard | Inside route | Conditional check |
transition | Inside machine | State transition fn |
command | Inside cli | CLI handler |
Next: State Machines — type-safe state transitions from 12 lines of KERN.