# Project export: Unreal EngJam

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: A cursed programming language and game engine inside of Figma.
- Devpost: https://devpost.com/software/unreal-engjam
- GitHub: https://github.com/kognise/aldente
- Video: https://www.youtube.com/embed/765IiE3aNFA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Cal Hacks: Most Entertaining Hack; Warp: Best Developer Tool by Warp)
- Team: 2 GitHub contributor(s) — Lexi Mattick (2 commits), JC (2 commits)

## Devpost submission (written by the team)

### Overview

Design engineers are the latest trend in the tech space, but nobody is talking about engineer designers. We're here to change that. Unreal EngJam (working title) is a concerningly robust, from-scratch programming language and game engine, designed from the ground up to be programmed in a Figma FigJam whiteboard. With our incredibly high quality and extremely spaghetti programming language, you can program such things as classic single-player pong... ... and even render 3D models inside Figma: Think of All the Benefits Why be limited to syntax highlighting when you can color-code your software? Whiteboard out math and algorithms in the same space as your business logic The visual programming appeal of Scratch, with the power and robustness of a real programming language Comments are first-class, graphical, and move around as you refactor (and refactoring is as easy as drag-and-drop) Clippy Make a mistake? Your favorite office assistant will hover above the relevant code and help you out: How We Built It We wrote the programming language entirely from scratch (no dependencies) in TypeScript. We use Figma's plugin API to traverse the document and generate an AST which we can then interpret. We provide the programmer with access to render to and manipulate a "game window" inside Figma. It is important to sufficiently emphasize that this is a full, statically-typed, dynamically-bound novel programming language designed explicitly for the medium of Figma flowcharts. For example, to define a variable you draw a box. To store values, you can draw an arrow to the box, and to read values you draw an arrow from the box. With arrows, nothing needs to be referenced by name and refactoring is easy. The assignment of function and infix operator arguments are inferred first by applied name and then internal type. Challenges We Ran Into Figma runs plugins in WASM which means it is extremely difficult to debug code, and simple bugs like stack overflows often result in cryptic low-level memory leak errors. Also, 3D models are hard to get right :)

## README (from the GitHub repository)

# Unreal EngJam (Working Title)

Design engineers are the latest trend in the tech space, but nobody is talking about engineer designers. We're here to change that.

Unreal EngJam is a concerningly robust, from-scratch programming language and game engine, designed from the ground up to be programmed in a Figma FigJam whiteboard.

With our incredibly high quality and extremely spaghetti programming language, you can program such things as classic single-player pong...

![pong](https://doggo.ninja/UVqBOT.png)

... and even render 3D models inside Figma:

![](https://doggo.ninja/oKQhg6.png)

## Think of All the Benefits

- Why be limited to syntax highlighting when you can color-code your software?
- Whiteboard out math and algorithms in the same space as your business logic
- The visual programming appeal of Scratch, with the power and robustness of a real programming language
- Comments are first-class, graphical, and move around as you refactor (and refactoring is as easy as drag-and-drop)

## Clippy

Make a mistake? Your favorite office assistant will hover above the relevant code and help you out:

![](https://doggo.ninja/erw3KX.png)

![](https://doggo.ninja/fQMIww.png)

## How We Built It

We wrote the programming language entirely from scratch (no dependencies) in TypeScript. We use Figma's plugin API to traverse the document and generate an AST which we can then interpret. We provide the programmer with access to render to and manipulate a "game window" inside Figma.

It is important to sufficiently emphasize that this is a full, statically-typed, dynamically-bound novel programming language designed explicitly for the medium of Figma flowcharts. For example, to define a variable you draw a box. To store values, you can draw an arrow to the box, and to read values you draw an arrow from the box. With arrows, nothing needs to be referenced by name and refactoring is easy.

The assignment of function and infix operator arguments are inferred first by applied name and then internal type.

## Challenges We Ran Into

Figma runs plugins in WASM which means it is extremely difficult to debug code, and simple bugs like stack overflows often result in cryptic low-level memory leak errors.

Also, 3D models are hard to get right :)

![broken](https://doggo.ninja/JNqtBT.png)


## Detected evidence (automated analysis)

Indexed codebase: 11 recognized source files, 98 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (17 of 17)

```
.eslintrc.json
.gitignore
manifest.json
package.json
README.md
src/ast.ts
src/clippy-data.ts
src/debug.ts
src/engine.ts
src/eval.ts
src/index.ts
src/input.ts
src/polyfills.ts
tsconfig.json
ui.html
webpack.config.js
window.json
```

### Dependencies

- package.json: @figma/eslint-plugin-figma-plugins@*, @figma/plugin-typings@*, @typescript-eslint/eslint-plugin@^6.12.0, @typescript-eslint/parser@^6.12.0, eslint@^8.54.0, ts-loader@^9.5.1, typescript@^5.6.3, webpack@^5.95.0, webpack-cli@^5.1.4

### Recent commits (newest first)

- committing final tweaks
- readme
- code
- hii
- code

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "unreal-engjam",
  "version": "1.0.0",
  "description": "Your Figma Plugin",
  "main": "dist.js",
  "scripts": {
    "build": "webpack",
    "dev": "yarn build --watch",
    "lint": "eslint --ext .ts,.tsx --ignore-pattern node_modules .",
    "lint:fix": "eslint --ext .ts,.tsx --ignore-pattern node_modules --fix ."
  },
  "author": "",
  "license": "UNLICENSED",
  "devDependencies": {
    "@figma/eslint-plugin-figma-plugins": "*",
    "@figma/plugin-typings": "*",
    "@typescript-eslint/eslint-plugin": "^6.12.0",
    "@typescript-eslint/parser": "^6.12.0",
    "eslint": "^8.54.0",
    "ts-loader": "^9.5.1",
    "typescript": "^5.6.3",
    "webpack": "^5.95.0",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {}
}

```

### src/index.ts

```typescript
import { compilePage } from './ast'
import { resetLoop } from './debug'
import { play, stop } from './eval'
import { interpretBrowserKeys, updatePressedKeys } from './input'
import { difference } from './polyfills'

async function main() {
	let previousButtons: Set<string> = new Set()
	figma.on('selectionchange', async () => {
		resetLoop()
		const windows = await compilePage(figma.currentPage)

		const buttons = new Set(figma.currentPage.selection.map(button => button.id))
		const newButtons = difference(buttons, previousButtons)
		previousButtons = buttons
		if (newButtons.size > 1) return

		const playWindows = new Set(windows.filter(window => window.playButtons.some(button => newButtons.has(button.id))))
		const stopWindows = new Set(windows.filter(window => window.stopButtons.some(button => newButtons.has(button.id))))

		for (const window of playWindows) await play(window)
		for (const window of stopWindows) await stop(window)
	})

	figma.showUI(__html__, { height: 100, width: 300 })

	figma.ui.on('message', (message) => {
		switch (message.kind) {
			case 'UPDATE_PRESSED_KEYS': {
				const rawPressedKeys = new Set(message.pressedKeys as string[])
				updatePressedKeys(interpretBrowserKeys(rawPressedKeys))
				break
			}
			default: console.warn('unknown message:', message)
		}
	})
}

main().catch(console.error)

```

### webpack.config.js

```javascript
const path = require('path')
const webpack = require('webpack')

module.exports = (env, argv) => ({
	mode: argv.mode === 'production' ? 'production' : 'development',
	devtool: argv.mode === 'production' ? false : 'inline-source-map',

	entry: {
		dist: './src/index.ts'
	},
	module: {
		rules: [
			{
				test: /\.tsx?$/,
				use: 'ts-loader',
				exclude: /node_modules/,
			},
		],
	},
	resolve: {
		extensions: ['.ts', '.js'],
	},
	output: {
		filename: '[name].js',
		path: __dirname,
	},
})

```

### ui.html

```html
<div id='container' tabindex='-1'></div>

<script>
	const container = document.getElementById('container')

	function renderContainer() {
		container.innerText = document.activeElement === container
			? 'you can use keyboard controls'
			: 'focus the plugin to use controls'
	}

	container.addEventListener('focus', renderContainer)
	container.addEventListener('blur', renderContainer)
	renderContainer()

	const pressedKeys = new Set()

	function updatePressedKeys() {
		parent.postMessage({ pluginMessage: { kind: 'UPDATE_PRESSED_KEYS', pressedKeys: [ ...pressedKeys ] } }, '*')
	}

	container.addEventListener('keydown', (event) => {
		pressedKeys.add(event.key)
		updatePressedKeys()
	})

	container.addEventListener('keyup', (event) => {
		pressedKeys.delete(event.key)
		updatePressedKeys()
	})
</script>

<style>
	html, body {
		margin: 0;
		height: 100%;
	}

	#container {
		border: 5px solid red;
		color: red;
		font-family: 'Comic Sans MS', sans-serif;
		text-align: center;
		height: 100%;
		box-sizing: border-box;
		outline: none;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		opacity: 0.5;
		cursor: default;
		user-select: none;
	}

	#container:focus {
		border-color: green;
		border-width: 15px;
		color: green;
		opacity: 1;
	}
</style>

```

### src/polyfills.ts

```typescript
export function difference<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([ ...a ].filter(x => !b.has(x)))
}

export function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([ ...a ].filter(x => b.has(x)))
}
```

### src/input.ts

```typescript
export const keys = [ 'up', 'down', 'left', 'right' ] as const

export type Key = typeof keys[number]

const pressedKeys: Set<Key> = new Set()

export function interpretBrowserKeys(browserKeys: Set<string>): Set<Key> {
	const keys: Set<Key> = new Set()

	for (const browserKey of browserKeys) {
		const key: Key | null = ({
			ArrowUp: 'up',
			ArrowDown: 'down',
			ArrowLeft: 'left',
			ArrowRight: 'right',
		} as const)[browserKey] ?? null

		if (key !== null) keys.add(key)
	}

	return keys
}

export function updatePressedKeys(newPressedKeys: Set<Key>): void {
	let dirty = false

	for (const key of newPressedKeys) {
		if (!pressedKeys.has(key)) {
			pressedKeys.add(key)
			dirty = true
		}
	}

	for (const key of pressedKeys) {
		if (!newPressedKeys.has(key)) {
			pressedKeys.delete(key)
			dirty = true
		}
	}
}

export function getPressedKeys(): Set<Key> {
	return pressedKeys
}

```

### src/engine.ts

```typescript
export interface SpriteEngine {
	kind: 'SPRITE'
	[key: string]: unknown
}

export interface WindowEngine {
	kind: 'WINDOW'
	width: number
	height: number
}

export interface TextEngine {
	kind: 'TEXT'
	[key: string]: unknown
}

export type Engine =
	| SpriteEngine
	| WindowEngine

export function clearWindow(window: SectionNode) {
	for (const child of window.children) {
		child.remove()
	}
}

export function makeWindow(window: SectionNode): WindowEngine {
	const windowEngine: WindowEngine = {
		kind: 'WINDOW',
		width: window.width,
		height: window.height
	}

	return new Proxy(windowEngine, {
		get(target, name, receiver) {
			windowEngine.width = window.width
			windowEngine.height = window.height
			if (Reflect.has(target, name)) {
				return Reflect.get(target, name, receiver)
			}
		},
		set(target, name, receiver) {
			if (Reflect.has(target, name)) {
				switch (name) {
					case 'width':
						window.resizeWithoutConstraints(receiver, windowEngine.height)
						break
					case 'height':
						window.resizeWithoutConstraints(windowEngine.width, receiver)
				}
				return Reflect.set(target, name, receiver)
			}
			return false
		}
	})
}

export function addSprite(window: SectionNode, graphic: SceneNode): SpriteEngine {
	const clone = graphic.clone()
	clone.x = 0
	clone.y = 0
	window.appendChild(clone)

	const spriteEngine: SpriteEngine = {
		kind: 'SPRITE',
	}

	return new Proxy(spriteEngine, {
		get(target, name, receiver) {
			if (Reflect.has(target, name)) {
				return Reflect.get(target, name, receiver)
			} else {
				const property = Object.getOwnPropertyDescriptor(
					Reflect.getPrototypeOf(clone),
					name
				)
				if (property !== undefined && property.get !== undefined)
					return property.get.call(clone)
			}
		},
		set(target, name, receiver) {
			switch (name) {
				case 'x':
					clone.x = receiver
					break
				case 'y':
					clone.y = receiver
			}
			return Reflect.set(target, name, receiver)
		}
	})
}

export async function addText(window: SectionNode): Promise<TextEngine> {
	const font = { family: 'Inter', style: 'Regular' }
	await figma.loadFontAsync(font)

	const text = figma.createText()
	text.fontName = font
	text.fontSize = 20
	text.fills = [ figma.util.solidPaint('#000000') ]
	window.appendChild(text)

	const textEngine: TextEngine = {
		kind: 'TEXT',
	}

	return new Proxy(textEngine, {
		get(target, name, receiver) {
			if (Reflect.has(target, name)) {
				return Reflect.get(target, name, receiver)
			} else {
				const property = Object.getOwnPropertyDescriptor(
					Reflect.getPrototypeOf(text),
					name
				)
				if (property !== undefined && property.get !== undefined)
					return property.get.call(text)
			}
		},
		set(target, name, receiver) {
			return Reflect.set(text, name, receiver)
		}
	})
}

```

### src/debug.ts

```typescript
import type { Asts } from './ast'
import { clippyHash, clippyBytes } from './clippy-data'

export class EvalError extends Error {
	originalMessage: string
	node: SceneNode

	constructor({ message, node }: { message: string, node: SceneNode }) {
		super(`${message} @ ${node.type} '${node.name}'`)
		this.originalMessage = message
		this.node = node
		this.name = 'EvalError'
	}
}

function getClippy(): Image {
	const clippy = figma.getImageByHash(clippyHash)
	if (!clippy) {
		const newClippy = figma.createImage(clippyBytes)
		if (newClippy.hash !== clippyHash) throw new Error('mismatching clippy hashes!')
		return newClippy
	}
	return clippy
}

export type ClippyVariant = 'WARNING' | 'ERROR'

const clippyPrefix = '__clippy__'

export async function clearClippies(variant: ClippyVariant): Promise<void> {
	for (const clippy of figma.currentPage.findAll((node) => node.name.startsWith(clippyPrefix + variant))) {
		clippy.remove()
	}
}

export async function makeClippyMessage(variant: ClippyVariant, message: string, on: SceneNode): Promise<SceneNode> {
	for (const existing of figma.currentPage.findAll(node => node.name.startsWith(clippyPrefix) && node.name.endsWith(on.id))) {
		existing.remove()
	}

	const clippy = getClippy()
	const parent = on.parent ?? figma.currentPage

	const image = figma.createRectangle()
	image.resize(62, 70)
	// parent.appendChild(image)
	image.fills = [
		{
			type: 'IMAGE',
			imageHash: clippy.hash,
			scaleMode: 'FILL'
		}
	]
	image.x = on.x + on.width - 25
	image.y = on.y - 87 + 25

	const font = { family: 'Roboto Mono', style: 'Medium' }
	await figma.loadFontAsync(font)

	const text = figma.createShapeWithText()
	text.shapeType = 'SQUARE'
	text.fills = [ figma.util.solidPaint(variant === 'WARNING' ? '#FFFFCC' : '#FFC7C2') ]
	text.text.fontName = font
	text.text.fontSize = 12
	text.text.fills = [ figma.util.solidPaint('#000000') ]
	text.resize(227, 87)
	text.text.characters = `${variant === 'WARNING' ? 'warning' : 'error'}: ${message}`
	text.x = image.x + 62
	text.y = image.y - 6.5

	const group = figma.group([ image, text ], parent)
	group.name = clippyPrefix + variant + on.id

	return group
}

export function WARN(message: string, node: SceneNode): void {
	console.warn(`${message} @ ${node.type} '${node.name}'`)
	void makeClippyMessage('WARNING', message, node)
}

export function ERROR(message: string, node: SceneNode): never {
	throw new EvalError({ message, node })
}

let _loop = 0
export function checkLoop() {
	if (++_loop >= 10000) throw new Error('Max Lexi stack exceeded.')
}

export function resetLoop() {
	_loop = 0
}

export function indent(text: string, levels: number): string {
	return text.split('\n').map(line => '  '.repeat(levels) + line).join('\n')
}

export function stringifyAst(node: Asts): string {
	switch (node.kind) {
		case 'WINDOW': {
			return [
				'window',
				indent(node.setup ? stringifyAst(node.setup) : 'no setup', 1),
				indent(node.loop ? stringifyAst(node.loop) : 'no loop', 1),
			].join('\n')
		}

		case 'FLOW': {
			return `flow '${node.name}'\n`
				+ indent(node.first ? stringifyAst(node.first) : 'no instructions', 1)
		}

		case 'INSTRUCTION': {
			const lines = [ stringifyAst(node.instruction) ]

			for (const input of node.inputs) lines.push(indent('← ' + stringifyAst(input), 1))
			for (const output of node.outputs) lines.push(indent('→ ' + stringifyAst(output), 1))

			if (node.matchArms) {
				for (const [ key, value ] of node.matchArms) {
					lines.push(indent(`match '${key}':`, 1))
					lines.push(indent(stringifyAst(value), 2))
				}
			}

			if (node.next) lines.push(stringifyAst(node.next))
			return lines.join('\n')
		}

		case 'GRAPHIC': {
			return '[graphic]'
		}

		case 'FILE': {
			return '[file]'
		}

		case 'INFIX': {
			let string = 'infix '
			if (node.left) string += `[${stringifyAst(node.left)}] `
			string += `${node.operator}`
			if (node.right) string += ` [${stringifyAst(node.right)}]`
			return string
		}

		case 'FUNCTION': {
			return `function '${node.text}'`
		}

		case 'LOOP': {
			return 'loop:\n'
				+ (node.body ? indent(stringifyAst(node.body), 1) : '(no body)')
		}

		case 'NUMBER': {
			return `number ${node.value}`
		}

		case 'STRING': {
			return `string '${node.value}'`
		}

		case 'VARIABLE': {
			return `variable '${node.name}' (${node.at.id})`
		}

		case 'PROPERTY': {
			let string = `property '${node.name}' of`
			if (node.parent === 'CURRENT_WINDOW') {
				string += ' current window'
			} else {
				string += ':\n' + indent(stringifyAst(node.parent), 1)
			}
			return string
		}
	}
}

```

### src/ast.ts

```typescript
import { checkLoop, clearClippies, WARN } from './debug'
import { type InfixOperator, infixOperators } from './eval'

export type Asts =
	| WindowAst
	| GraphicAst
	| FileAst
	| InstructionAst
	| FlowAst
	| FunctionAst
	| NumberAst
	| StringAst
	| InfixAst
	| VariableAst
	| PropertyAst
	| LoopAst
export type AstByKind<Kind extends Asts['kind']> = Extract<Asts, { kind: Kind }>

export interface WindowAst {
	kind: 'WINDOW'
	playButtons: SceneNode[]
	stopButtons: SceneNode[]
	setup: FlowAst | null
	loop: FlowAst | null
	at: SectionNode
}

export interface GraphicAst {
	kind: 'GRAPHIC'
	at: SceneNode
}

export interface FileAst {
	kind: 'FILE'
	data: string
	at: SceneNode
}

export interface FunctionAst {
	kind: 'FUNCTION'
	text: string
	at: TextNode
}

export interface InfixAst {
	kind: 'INFIX'
	operator: InfixOperator
	left: NumberAst | StringAst | PropertyAst | null
	right: NumberAst | StringAst | PropertyAst | null
	at: TextNode
}

export interface NumberAst {
	kind: 'NUMBER'
	value: number
	at: SceneNode
}

export interface StringAst {
	kind: 'STRING'
	value: string
	at: SceneNode
}

export interface VariableAst {
	kind: 'VARIABLE'
	name: string
	propertyInitializer: PropertyAst | null
	at: ShapeWithTextNode
}

export interface PropertyAst {
	kind: 'PROPERTY'
	name: string
	parent: DataAsts | 'CURRENT_WINDOW'
	at: SceneNode
}

export interface LoopAst {
	kind: 'LOOP'
	body: InstructionAst | null
	at: TextNode
}

export type InstructionInnerAsts =
	| FunctionAst
	| NumberAst
	| StringAst
	| InfixAst
	| LoopAst

export interface InstructionAst {
	kind: 'INSTRUCTION'
	instruction: InstructionInnerAsts
	inputs: DataAsts[]
	outputs: DataAsts[]
	matchArms: Map<string, InstructionAst> | null
	next: InstructionAst | null
	at: TextNode
}

export type DataAsts =
	| FlowAst
	| GraphicAst
	| FileAst
	| VariableAst
	| PropertyAst
	| NumberAst
	| StringAst

export interface FlowAst {
	kind: 'FLOW'
	name: string
	first: InstructionAst | null
	at: TextNode
}

export async function compilePage(page: PageNode): Promise<WindowAst[]> {
	await clearClippies('WARNING')

	const windows: WindowAst[] = []

	const ctx: CompileContext = {}

	for (const node of page.children) {
		if (node.type !== 'SECTION') continue
		windows.push(await compileWindow(node, ctx))
	}

	return windows
}

interface CompileContext {}

interface ConnectorDirections {
	incomingArrows: SceneNode[]
	outgoingArrows: SceneNode[]
	next: SceneNode[]
}

async function getConnections(node: SceneNode): Promise<ConnectorDirections> {
	checkLoop()

	const incomingArrows: SceneNode[] = []
	const outgoingArrows: SceneNode[] = []
	const next: SceneNode[] = []

	for (const connector of node.attachedConnectors) {
		if (!('endpointNodeId' in connector.connectorStart)) continue
		if (!('endpointNodeId' in connector.connectorEnd)) continue
		if (connector.connectorStart.endpointNodeId === connector.connectorEnd.endpointNodeId) continue

		const polarized = connector.connectorStart.endpointNodeId === node.id
			? {
				thisCap: connector.connectorStartStrokeCap,
				otherNode: (await figma.getNodeByIdAsync(connector.connectorEnd.endpointNodeId)) as SceneNode,
				otherCap: connector.connectorEndStrokeCap
			}
			: {
				thisCap: connector.connectorEndStrokeCap,
				otherNode: (await figma.getNodeByIdAsync(connector.connectorStart.endpointNodeId)) as SceneNode,
				otherCap: connector.connectorStartStrokeCap
			}

		if (polarized.otherCap !== 'NONE') {
			outgoingArrows.push(polarized.otherNode)
		} else if (polarized.thisCap !== 'NONE') {
			incomingArrows.push(polarized.otherNode)
		} else if (connector.connectorEnd.endpointNodeId !== node.id) {
			next.push((await figma.getNodeByIdAsync(connector.connectorEnd.endpointNodeId)) as SceneNode)
		}
	}

	// Sort next top-to-bottom.
	next.sort((a, b) => a.y - b.y)

	return { incomingArrows, outgoingArrows, next }
}

async function compileData(node: SceneNode, ctx: CompileContext): Promise<DataAsts | null> {
	if (node.type === 'SHAPE_WITH_TEXT') {
		const text = node.text.characters.trim()

		if (text.length > 0) {
			if (node.shapeType === 'SQUARE') {
				const { incomingArrows } = await getConnections(node)
				const inputs = []
				for (const incomingArrow of incomingArrows) {
					if (incomingArrow.type !== 'SHAPE_WITH_TEXT' || incomingArrow.shapeType !== 'ELLIPSE') continue
					const input = await compileData(incomingArrow, ctx)
					if (input) inputs.push(input)
				}

				return {
					kind: 'VARIABLE',
					name: text,
					propertyInitializer: inputs.find(input => input.kind === 'PROPERTY') ?? null,
					at: node,
				}
			}

			if (node.shapeType === 'ELLIPSE') {
				// Property.

				const { incomingArrows } = await getConnections(node)

				const validParents: DataAsts[] = []
				for (const incomingArrow of incomingArrows) {
					if (incomingArrow.type === 'SHAPE_WITH_TEXT') {
						const data = await compileData(incomingArrow, ctx)
						if (data) validParents.push(data)
					}
				}

				if (validParents.length > 1) {
					WARN(`property '${text}' has more than one valid parents, only one will be used.`, node)
				}

				return {
					kind: 'PROPERTY',
					name: text,
					parent: validParents[0] ?? 'CURRENT_WINDOW',
					at: node,
				}
			}

			if (node.shapeType === 'ENG_DATABASE') {
				// File.

				return {
					kind: 'FILE',
					data: text,
					at: node
				}
			}
		}

		return {
			kind: 'GRAPHIC',
			at: node,
		}
	}

	if (node.type === 'TEXT') return await compileFlow(node, ctx)

	return {
		kind: 'GRAPHIC',
		at: node,
	}
	// WARN(`could not interpret this data, it will be ignored.`, node)
	// return null
}

function tryParseAsNumber(text: string): number | null {
	text = text.trim()
	if (!/^-?(?:\d*\.)?\d+$/.test(text)) return null
	return parseFloat(text)
}

function tryParseAsString(text: string): string | null {
	text = text.trim()
	if (!/^[/“”"/].*[/“”"/]$/.test(text)) return null
	return text.slice(1, -1)
}

async function compileInfixSide(text: string, _ctx: CompileContext, at
[truncated — 5939 more characters]
```

### src/eval.ts

```typescript
import type { FlowAst, DataAsts, InstructionAst, WindowAst } from './ast'
import { clearClippies, ERROR, EvalError, makeClippyMessage, resetLoop, WARN } from './debug'
import { clearWindow, addSprite, type SpriteEngine, WindowEngine, makeWindow, TextEngine, addText } from './engine'
import { getPressedKeys, keys } from './input'
import { intersection } from './polyfills'

export const booleanType: EnumType = {
	kind: 'ENUM_TYPE',
	options: new Set([ 'yes', 'no' ]),
}
export function booleanTrue(at: SceneNode): EnumObj {
	return {
		kind: 'ENUM_OBJ',
		type: booleanType,
		selected: new Set([ 'yes' ]),
		at,
	}
}
export function booleanFalse(at: SceneNode): EnumObj {
	return {
		kind: 'ENUM_OBJ',
		type: booleanType,
		selected: new Set(['no']),
		at,
	}
}

export interface NumberType {
	kind: 'NUMBER_TYPE'
}

export interface StringType {
	kind: 'STRING_TYPE'
}

export interface GraphicType {
	kind: 'GRAPHIC_TYPE'
}

export interface SpriteType {
	kind: 'SPRITE_TYPE'
}

export interface TextType {
	kind: 'TEXT_TYPE'
}

export interface EnumType {
	kind: 'ENUM_TYPE'
	options: Set<string>
}

export interface GraphicType {
	kind: 'GRAPHIC_TYPE'
}

export interface FlowType {
	kind: 'FLOW_TYPE'
}

export interface ArrayType {
	kind: 'ARRAY_TYPE'
	item: Type
}

export interface AnyType {
	kind: 'ANY_TYPE'
}

export type Type =
	| NumberType
	| StringType
	| SpriteType
	| TextType
	| EnumType
	| GraphicType
	| FlowType
	| ArrayType
	| AnyType

// ---

export interface NumberObj {
	kind: 'NUMBER_OBJ'
	type: NumberType
	value: number
	at: SceneNode
}

export interface StringObj {
	kind: 'STRING_OBJ'
	type: StringType
	value: string
	at: SceneNode
}

export interface GraphicObj {
	kind: 'GRAPHIC_OBJ'
	type: GraphicType
	graphic: SceneNode
	at: SceneNode
}

export interface FlowObj {
	kind: 'FLOW_OBJ'
	type: FlowType
	node: FlowAst
	at: SceneNode
}

export interface SpriteObj {
	kind: 'SPRITE_OBJ'
	type: SpriteType
	engine: SpriteEngine
	at: SceneNode
}

export interface TextObj {
	kind: 'TEXT_OBJ'
	type: TextType
	engine: TextEngine
	at: SceneNode
}

export interface EnumObj {
	kind: 'ENUM_OBJ'
	type: EnumType
	selected: Set<string>
	at: SceneNode
}

export interface ArrayObj {
	kind: 'ARRAY_OBJ'
	type: ArrayType
	items: Obj[]
	at: SceneNode
}

export type Obj =
	| NumberObj
	| StringObj
	| SpriteObj
	| EnumObj
	| GraphicObj
	| TextObj
	| FlowObj
	| ArrayObj

export type ObjOfType<T extends Type> = Extract<Obj, { type: T }>

// ---

const stopFunctions: Map<string, () => void> = new Map()
const variables: Map<string, Obj> = new Map()

interface EvalContext {
	windowEngine: WindowEngine
	windowNode: SectionNode
	isDone: boolean
}

function getDataValue(data: DataAsts, ctx: EvalContext): Obj {
	switch (data.kind) {
		case 'GRAPHIC': {
			return {
				kind: 'GRAPHIC_OBJ',
				type: { kind: 'GRAPHIC_TYPE' },
				graphic: data.at,
				at: data.at
			}
		}
		case 'VARIABLE': {
			return variables.get(data.at.id)
				?? (data.propertyInitializer && getDataValue(data.propertyInitializer, ctx))
				?? ERROR(`variable '${data.name}' is not set.`, data.at)
		}
		case 'PROPERTY': {
			try {
				const evil: unknown = data.parent === 'CURRENT_WINDOW'
					? ctx.windowEngine
					: data.parent.kind === 'VARIABLE'
						// @ts-expect-error jank
						? getObjPrimitiveValue(variables.get(data.parent.at.id))
						: undefined
				return {
					kind: 'NUMBER_OBJ',
					type: { kind: 'NUMBER_TYPE' },
					// @ts-expect-error jank
					value: evil[data.name],
					at: data.at
				}
			} catch (error) {
				console.error('error reading property:')
				console.error(error)
				return ERROR('failed to read variable', data.at)
			}
		}
		case 'FLOW': {
			return {
				kind: 'FLOW_OBJ',
				type: { kind: 'FLOW_TYPE' },
				node: data,
				at: data.at
			}
		}
		case 'NUMBER': {
			return {
				kind: 'NUMBER_OBJ',
				type: { kind: 'NUMBER_TYPE' },
				value: data.value,
				at: data.at
			}
		}
		case 'STRING': {
			return {
				kind: 'STRING_OBJ',
				type: { kind: 'STRING_TYPE' },
				value: data.value,
				at: data.at
			}
		}
		case 'FILE': {
			return {
				kind: 'STRING_OBJ',
				type: { kind: 'STRING_TYPE' },
				value: data.data,
				at: data.at
			}
		}
	}
}

type PickArgsInput = { name: string | null, obj: Obj }

function typesEq(a: Type, b: Type): boolean {
	if (a.kind === 'ANY_TYPE' || b.kind === 'ANY_TYPE') return true
	if (a.kind !== b.kind) return false
	if (a.kind === 'ENUM_TYPE' && b.kind === 'ENUM_TYPE') {
		if (a.options.size !== b.options.size) return false
		if (intersection(a.options, b.options).size !== a.options.size) return false
	}
	if (a.kind === 'ARRAY_TYPE' && b.kind === 'ARRAY_TYPE') {
		return typesEq(a.item, b.item)
	}
	return true
}

function pickArgs(required: FnArg[], inputs: PickArgsInput[], at: SceneNode): Obj[] {
	inputs = [ ...inputs ]

	const args: Obj[] = []

	// Use named arguments.
	for (let i = 0; i < required.length; i++) {
		if (required[i].name === null) continue
		const inputIndex = inputs.findIndex(input => input.name === required[i].name)
		if (inputIndex === -1) continue
		args[i] = inputs[inputIndex].obj
		inputs.splice(inputIndex, 1)
	}

	// Process typed arguments.
	for (let i = 0; i < required.length; i++) {
		if (args[i]) continue
		const inputIndex = inputs.findIndex(input => typesEq(input.obj.type, required[i].type))
		if (inputIndex === -1) continue
		args[i] = inputs[inputIndex].obj
		inputs.splice(inputIndex, 1)
	}

	for (const input of inputs) WARN('extraneous input has been ignored.', input.obj.at)

	// Ensure we have all arguments.
	for (let i = 0; i < required.length; i++) {
		if (args[i]) continue

		let message = `missing argument of type '${required[i].type.kind}'`
		if (required[i].name !== null) message += ` with name '${required[i].name}'`
		message += ` at position ${i}.`

		throw new EvalError({ message, node: at })
	}

	return args
}

async function getInstructionValue(instruction: InstructionAst, ctx: EvalContext): Promise<Obj | null> {
	// console
[truncated — 16039 more characters]
```