Perry Constraints (Detailed)
Perry is an AOT (ahead-of-time) compiler that transforms TypeScript directly into native machine code. Because there is no runtime interpreter or JIT, certain JavaScript/TypeScript patterns that rely on runtime dynamism are not supported. This guide documents every constraint with explanations and corrected code.
1. Dynamic Key Access
Pattern: obj[variable]
Perry resolves all property accesses at compile time. A dynamic key like obj[key] requires runtime dispatch to determine which property to read — Perry has no mechanism for this.
// BROKEN — Perry cannot resolve the property at compile time
function getConfig(config: Config, key: string): string {
return config[key];
}
// FIXED — enumerate every possible key explicitly
function getConfig(config: Config, key: string): string {
if (key === 'host') return config.host;
else if (key === 'port') return config.port;
else if (key === 'protocol') return config.protocol;
return '';
}
This also applies to dynamic writes (obj[key] = value) and computed property access on arrays of objects.
2. Optional Chaining (?.)
Pattern: obj?.prop, arr?.[0], fn?.()
Perry does not implement the optional chaining operator. The compiler will fail or produce incorrect code.
// BROKEN
const name = user?.profile?.name;
// FIXED — explicit null/undefined checks
let name = '';
if (user !== null && user !== undefined) {
if (user.profile !== null && user.profile !== undefined) {
name = user.profile.name;
}
}
Every level of the chain needs its own guard. This is verbose but produces correct native code.
3. Nullish Coalescing (??)
Pattern: value ?? fallback
Not implemented in Perry’s codegen. The operator is not recognized.
// BROKEN
const port = config.port ?? 8080;
// FIXED
let port = 8080;
if (config.port !== undefined) {
port = config.port;
}
If you also need to guard against null:
let port = 8080;
if (config.port !== null && config.port !== undefined) {
port = config.port;
}
4. Regular Expressions
Pattern: /regex/, RegExp, .test(), .match(), .replace() with regex
Perry has no regex engine. Any code that constructs or uses a RegExp will fail.
// BROKEN
if (/^[a-z]+$/.test(input)) {
// ...
}
// FIXED — manual character checking
const ALPHA = 'abcdefghijklmnopqrstuvwxyz';
let allAlpha = true;
for (let i = 0; i < input.length; i++) {
if (ALPHA.indexOf(input.charAt(i)) < 0) {
allAlpha = false;
break;
}
}
For string splitting or searching, use indexOf, substring, and manual parsing loops.
// BROKEN
const parts = line.split(/\s+/);
// FIXED — split on single space, trim first
const trimmed = line.trim();
const parts: string[] = [];
let start = 0;
for (let i = 0; i <= trimmed.length; i++) {
if (i === trimmed.length || trimmed.charAt(i) === ' ') {
if (i > start) {
parts.push(trimmed.substring(start, i));
}
start = i + 1;
}
}
5. ES6 Shorthand Properties
Pattern: { name, age }
Perry requires explicit key-value pairs in object literals.
// BROKEN
const name = 'Alice';
const age = 30;
const user = { name, age };
// FIXED
const user = { name: name, age: age };
This applies everywhere: return statements, function arguments, variable declarations.
6. Array.map on Class Fields
Pattern: this.items.map(fn)
Perry cannot dispatch .map() on arrays stored as class instance fields. The method resolution fails at compile time.
// BROKEN
class UserList {
users: User[] = [];
getNames(): string[] {
return this.users.map(u => u.name);
}
}
// FIXED — use a for loop
class UserList {
users: User[] = [];
getNames(): string[] {
const names: string[] = [];
for (let i = 0; i < this.users.length; i++) {
names.push(this.users[i].name);
}
return names;
}
}
This also applies to .filter(), .reduce(), .find(), and other higher-order array methods on class fields. Use indexed for loops instead.
7. for...of on Arrays
Pattern: for (const item of items)
Perry does not implement the iterator protocol. for...of loops will not work on arrays.
// BROKEN
for (const item of items) {
process(item);
}
// FIXED
for (let i = 0; i < items.length; i++) {
const item = items[i];
process(item);
}
Also avoid for...in on objects — use explicit property access instead.
8. Character Range Comparisons
Pattern: c >= 'a' && c <= 'z'
Perry does not support ordering comparisons on characters (strings of length 1). The comparison operators work on numbers, not character codes.
// BROKEN
function isLowerAlpha(c: string): boolean {
return c >= 'a' && c <= 'z';
}
// FIXED
const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
function isLowerAlpha(c: string): boolean {
return LOWER_ALPHA.indexOf(c) >= 0;
}
For digits:
const DIGITS = '0123456789';
function isDigit(c: string): boolean {
return DIGITS.indexOf(c) >= 0;
}
9. Closure Capture Semantics
Pattern: Closures that read or mutate outer variables
Perry captures closure variables by value at the time the closure is created. Any subsequent mutation to the outer variable is invisible to the closure, and any mutation inside the closure does not affect the outer variable.
// BROKEN — closure captures initial value of this.count
class Counter {
count = 0;
setup() {
setInterval(() => {
this.count++; // increments a stale copy, not the real field
console.log(this.count);
}, 1000);
}
}
// FIXED — module-level state with named functions
let _count = 0;
function incrementCount() {
_count++;
console.log(_count);
}
setInterval(incrementCount, 1000);
Rules:
- Store mutable state in module-level
letvariables. - Access that state through module-level named functions (not closures).
- Never capture
thisin a closure passed tosetInterval, event handlers, or callbacks.
10. requestAnimationFrame
Pattern: requestAnimationFrame(callback)
RAF never fires in Perry. The Perry runtime does not have a browser-style event loop with frame callbacks.
// BROKEN — callback never executes
function animate() {
updateFrame();
requestAnimationFrame(animate);
}
// FIXED
setInterval(updateFrame, 16); // ~60fps
11. setTimeout Self-Recursion
Pattern: setTimeout called inside its own callback
When setTimeout is called inside the callback of a previous setTimeout, the new timeout only fires once and then stops. This breaks recursive polling patterns.
// BROKEN — fires once, then stops
function poll() {
doWork();
setTimeout(poll, 1000);
}
setTimeout(poll, 1000);
// FIXED
setInterval(doWork, 1000);
If you need to stop the interval later, capture the return value:
const id = setInterval(doWork, 1000);
// later:
clearInterval(id);
12. String-Returning Functions in Async Context
Pattern: Returning a string from an async function or a function called in async context
String values returned from async contexts are NaN-boxed StringHeader pointers. When the receiving code treats the return value as a string, it gets a numeric pointer instead.
// BROKEN
async function getName(): Promise<string> {
return fetchName(); // returns NaN-boxed pointer
}
// FIXED — inline the string operation at the call site
// instead of returning strings through async boundaries
Where possible, perform string operations directly where the result is needed rather than passing strings across async boundaries.
13. new Date() in Async Context
Pattern: new Date() inside an async function
The Date constructor malfunctions in async contexts due to how Perry handles object allocation across execution frames.
// BROKEN
async function getTimestamp(): Promise<number> {
const now = new Date();
return now.getTime();
}
// FIXED — Date.now() returns a plain number, no object allocation
async function getTimestamp(): Promise<number> {
return Date.now();
}
Date.now() is safe everywhere because it returns a primitive number, avoiding the object allocation issue.
Quick Reference
| Avoid | Use Instead |
|---|---|
obj[variable] | if/else if per key |
?. optional chaining | Explicit null checks |
?? nullish coalescing | if (x !== undefined) |
/regex/.test() | indexOf or char checks |
{ key } shorthand | { key: key } |
this.arr.map(fn) | for loop |
for...of on arrays | for (let i = 0; ...) |
c >= 'a' && c <= 'z' | ALPHA.indexOf(c) >= 0 |
Closures capturing this | Module-level functions + state |
requestAnimationFrame | setInterval |
setTimeout recursion | setInterval |
| String returns in async | Inline string ops |
new Date() in async | Date.now() |