pyreon

PMTC Supported TypeScript Surface

Status: Phase D follow-up of the 2026-06 native readiness audit. Scout-8 scored this surface 18/100 — the lowest of any item — because there was no enumeration of what TS shapes PMTC accepts, drops, or warns on. This page closes that gap.

The Pyreon Multi-Target Compiler (PMTC) intercepts JSX + TypeScript source in your .tsx files and emits Swift (SwiftUI) and Kotlin (Compose). It does not compile arbitrary TypeScript — Phase 0 deliberately ships a focused subset that covers the seven canonical patterns the TodoMVC + native-counter examples exercise. This page enumerates exactly what works, what silently drops, what fires a warning, and what's planned.

If you hit a shape this page doesn't list, treat it as undefined behavior — file an issue with the source snippet so the matrix can grow.

The accepted-shape catalogue

✅ Type aliases

type Todo = { id: number; text: string; done: boolean }
type Filter = 'all' | 'active' | 'completed'
ShapeStatusNotes
Inline object type alias (type X = { a: T; b: U })✅ FullEmitted as struct (Swift) / data class (Kotlin)
Union of string literals (type X = 'a' | 'b')✅ FullEmitted as enum on both targets
Aliased primitive (type Id = number)🟡 SkippedFalls through silently when the alias is a primitive; consumer code uses the underlying type directly
Union with non-string-literal members🟡 SkippedFalls through silently when the alias contains a non-literal union member
Generic type aliases (type Box<T> = …)❌ Phase 3Generic type parameters are explicitly skipped
Empty object type (type X = {})❌ Skipped + warning"Struct X: skipped — empty object type."
Empty string in union branch❌ Skipped + warning"Enum X: skipped empty-string union branch."

✅ Module-level bindings

let nextId = 1
const TAX_RATE = 0.08
ShapeStatusNotes
let x = literal at module scope✅ FullEmitted as private var x = … (Swift) / var x = … (Kotlin); module-level mutable state
const x = literal at module scope✅ FullEmitted as private let x = … / val x = …
Module-level call expressions (const x = makeThing())🟡 SkippedFalls through silently unless the callee is a recognised hook (signal/computed/etc.)
export const x = …✅ FullSame as the unexported form; export keyword tolerated

✅ Functional components

export function Greeting(props: { title: string; count: number }) {
  return <Stack><Text>{props.title}: {props.count}</Text></Stack>
}
ShapeStatusNotes
function Comp(props: { … })✅ FullThe canonical shape
const Comp = (props: { … }) => …✅ FullArrow form supported
function Comp(props) (untyped)❌ WarningComponent X has an untyped 'props' parameter — type-annotate it (e.g. function X(props: { title: string })) — PR #1136. Without annotation, member access (props.X) silently drops.
function Comp({ a, b }: { a: string; b: number }) (destructured)🟡 SkippedThe destructured-param shape bails earlier in parseProps — no warning yet (planned).
export default function Comp(props)✅ FullDefault export supported
Component with no return statement❌ Warning"Component X: no return statement found; skipping."
Component returning a fragment (<>…</>)🟡 Phase 1+Limited — single root element preferred

✅ Hooks (component-body declarations)

The compiler recognises these hook identifiers and emits per-target equivalents:

HookShape supportedTarget emit
signal<T>(initial)const x = signal(0) / signal<string>('')@State (Swift) / var x by remember { mutableStateOf(…) } (Kotlin)
computed(() => expr)const c = computed(() => a() + b())Computed property (Swift) / derivedStateOf (Kotlin)
useStorage<T>('key', default)Persistent signal@PyreonAppStorage (Swift) / rememberPyreonStorage (Kotlin)
useFetch<T>('/url')URLSession/ktor wrapperPyreonFetch<T> runtime container
useForm({ initialValues })Form-state containerPyreonForm runtime container — initialValues must be a literal object map
useOnline()Network-status signalPyreonNetworkStatus container
usePermissions(['perm.X', 'perm.Y'])RBAC bridgePyreonPermissions container; identifier args silently dropped — only string literals captured
useClipboard()Clipboard containerPyreonClipboard container
useColorScheme()Reactive light/dark@Environment(\.colorScheme) (Swift) / isSystemInDarkTheme() (Kotlin)

Hooks: silent-drop shapes

  • useStorage<unknown>('k', '') (no inferred type) — silently emits unbound T. Always pass the generic.

  • useFetch('/url') without a generic — silently drops the decode type; emit may compile but result.data is Unknown.

  • useForm({ initialValues: x }) where x is not a literal object — silently dropped. Use useForm({ initialValues: { foo: 0 } }).

  • usePermissions([myString]) where myString is an identifier — silently dropped. Pass string literals or migrate to runtime permissions.set().

Hooks: warning shapes (PR #1136, A3)

  • useLoaderData<T>()A3 diagnostic (PR #1235). Emits a warning naming the binding because PMTC has no emit yet; the runtime PyreonRouter.setLoaderData() is the only way to populate this signal today. (Real emit lands as Phase B.6.)

  • const { copy, copied } = useClipboard() — destructure form unsupported; warns to use const cb = useClipboard() instead.

✅ Function declarations

function deleteAt(index: number) { items.value.splice(index, 1) }
ShapeStatusNotes
const fn = (a, b) => expr✅ FullExpression-body arrow
const fn = (a, b) => { ... }✅ FullBlock-body arrow
function fn(a, b) { ... }✅ FullFunction declaration (PR landed Round-1)
async function / async () => …🟡 Phase 1Async body must be inside a recognised effect hook
Default parameters (function fn(a = 1))🟡 Phase 2May silently drop the default value
Rest parameters (function fn(...args))❌ UnsupportedSilent drop

✅ Reactive prop access

function Foo(props: { title: string; count: number }) {
  return <Text>{props.title}: {props.count}</Text>
}

The props.X member access is rewritten per target. The annotation type is the source of truthprops.unknown (a field not in the annotation) silently emits an unbound reference. Always type-annotate.

ShapeStatusNotes
props.fieldName for an annotated field✅ FullRewritten to platform-native field
props.fieldName for an UN-annotated field❌ Silent dropField not in annotation = parser doesn't know about it. No warning at this granularity (only for the parent — see "untyped props parameter" above)
Destructure (const { title } = props)🟡 SkippedFalls through silently — not yet emitted; use props.title directly
Spread attributes (e.g. Child element with {...props})❌ UnsupportedSilently dropped (the spread attribute is ignored; explicit attrs win)

✅ Routing

const router = createRouter({
  routes: [
    { path: '/', component: HomePage },
    { path: '/users/:id', component: UserPage, beforeEnter: () => isAuthed() },
  ],
  beforeEach: [authGuard, logGuard],
  afterEach: [analytics],
})
return <RouterProvider router={router}><RouterView /></RouterProvider>
ShapeStatusNotes
createRouter({ routes: [...] })✅ FullRoutes extracted; non-literal arrays silently drop
{ path: '/x', component: Identifier }✅ FullBoth fields required; non-literal path silently dropped
beforeEnter: () => expr (expression-body)✅ FullA5 PR #1242 wired into runtime
beforeEnter: () => { … } (block-body)❌ WarningPR #1136 — "Per-route beforeEnter is a block-body arrow — only expression-body arrows are extracted; this route emits UNGUARDED."
beforeEach: [identifier, identifier]✅ FullA4-shipped runtime; identifier args land, inline-arrow args silently dropped
afterEach: [identifier]✅ FullSame shape as beforeEach
children: [...]✅ FullA4.5 PR #1243 — nested routes with depth-tracked <RouterView />
notFoundComponent: Identifier✅ FullA6 PR #1239 — wildcard-404 fallback
redirect: '/login'🟡 Phase B6Not yet extracted; use router.redirect('/login') at runtime
meta: {...} / name: 'x'❌ Silently ignoredDocumented as Phase B+ — extra fields drop without diagnostic
loader: async ({ params }) => …❌ Silently ignoredCompiler skips the field; runtime useLoaderData<T>() fires A3 diagnostic instead. Real emit = Phase B6.

✅ JSX

<Stack space="md">
  <Text>Hello {name}</Text>
  <For each={items} by={(i) => i.id}>{(item) => <Text>{item.text}</Text>}</For>
</Stack>
ShapeStatusNotes
Canonical primitive element (<Stack> etc — all 15)✅ FullPer target via canonical-primitives.ts SWIFT_NAMES / KOTLIN_NAMES
Component element (<UserPage>)✅ FullResolved via local function declarations
Static text child✅ FullLiteral strings, numbers
Expression child {expr}✅ FullMember access, function calls, signal reads
<For each={…} by={…}>✅ FullKeyed iteration emit; by required (else each doesn't typecheck on either target)
<Show when={…}>✅ FullGate emit
{...props} spread❌ Silently ignoredThe spread is dropped; explicit attrs win
Conditional {cond && <X>}✅ FullStandard JSX shape
Ternary {cond ? <A /> : <B />}✅ FullStandard JSX shape
Element fragment (<>…</>)🟡 Phase 1+Wrap in <Stack> for now
Hook calls inside JSX expressions (<For>{() => { const x = signal(0); …}} )❌ Warning (PR #1136)"Hook signal(…) declared inside <For> render callback — PMTC only extracts hooks at component-body scope. Lift the declaration to the parent component."

Comprehensive silent-drop catalogue

These are shapes the parser walks but doesn't (yet) emit anything for. The compiler doesn't fire a diagnostic — code compiles and runs, but the silently-dropped shape contributes nothing to the native output. Track at parse.ts (search "silently" / "drop" / "Phase").

ShapeWhere in parse.tsWhy silentWorkaround
Module-level destructured const { a, b } = objline ~98Phase 3 — not enumeratedUse const a = obj.a; const b = obj.b;
Type alias with non-literal union memberline ~49Falls through to "complex" pathInline the literal members
Aliased primitive typeline ~190No struct to emitUse the primitive directly
Class declarations(not handled)Not in Phase 0 scopeUse function components
Module-level non-hook const x = call()line ~501Not a recognised patternHoist into a hook the compiler knows
Imports beyond known runtime/JSX modulesn/aPhase 0 doesn't follow importsInline used identifiers
function-declaration inside another functionline ~573Not recognised at body scopeUse const fn = () => …
useStorage without explicit genericline ~520No type info to emitAlways pass useStorage<MyType>(…)
useForm without literal initialValuesline ~700Cannot inspect computed valuesInline the literal initialValues
usePermissions with non-string-literal argline ~676Identifier value invisible to parserPass string literals
Route's loader: async (ctx) => … fieldline ~872Loader auto-emit deferred to Phase B6Use runtime router.setLoaderData() + the A3 diagnostic for useLoaderData<T>()
Route's meta, nameline ~862Not in v1 scopeTrack app-side; use route path as key
Inline-arrow guards in beforeEach/afterEachline ~597Identifier-array parser onlyHoist to named function, then pass identifier
Spread attribute on JSX (<Comp {...props}>)(compiler-level)Phase 2 follow-upForward attrs explicitly
Block-body beforeEnter arrowline ~862A5 wires expression-body only; block-body warns (PR #1136)Use expression body: beforeEnter: () => isAuthed()
useClipboard destructureline ~459Warns (PR #1136)Use single-binding shape

Diagnostic warnings (already fire today)

These warnings ship in parse.ts and surface via result.warnings from @pyreon/native-compiler's transform(). The CLI builder (pyreon-native build) also prints them to stderr.

  1. Untyped props parameter (PR #1136) — function X(props) { ... props.title ... } with no annotation.

  2. useClipboard destructure form (PR #1136) — const { copy } = useClipboard().

  3. Block-body per-route beforeEnter (PR #1136) — beforeEnter: () => { ... } (only expression-body arrows are extracted).

  4. Hook inside render callback (PR #1136) — <For>{(item) => { const x = signal(0); ... }}.

  5. Round-1: missing required props (PR #1094) — Icon/Image/Link without name/src/to.

  6. Round-2: silent-drop shapes (PR #1099) — Press without onPress, Link prefetch=… on native, Stack/Inline/Layer align="<typo>".

  7. A3: useLoaderData<T>() (PR #1235) — silent-drop diagnostic naming the binding; runtime emit is Phase B6.

Consuming compiler diagnostics

import { transform } from '@pyreon/native-compiler'

const result = transform(source, { target: 'swift' })
console.log(result.code)        // emitted Swift
console.log(result.warnings)    // ['Component X has an untyped …', …]

The CLI (pyreon-native build) prints [pyreon-native] N warning(s): to stderr automatically. No Vite-plugin or LSP/editor surfacer exists yet — that's a Phase D6 follow-up.

What's NOT supported (and not planned for Phase 0)

The audit's Phase B/C/D roadmap explicitly does NOT cover:

  • Class components — Pyreon's web side hasn't shipped classes either; not a multi-target concern.

  • Hooks rules (call-from-render-context-only, etc.) — Pyreon doesn't have React's hook rules; the compiler-level constraint is "hook declarations live at component body scope".

  • JSX namespacing (e.g. svg:rect element prefix syntax) — not in v1.

  • Generic type parameters on user types — explicitly Phase 3 work.

  • Conditional types (T extends U ? A : B) — not parsed; treat as opaque.

  • Decorators — not in v1.

  • Higher-order components (HOC pattern) — partially possible if the HOC is just a function returning a component, but the type-flow gets lost.

If you need any of the above, file an issue with the source pattern; the matrix grows from real-world demand.

Cross-references

PMTC Supported TypeScript Surface