master
Dustin Swan 2 weeks ago
parent 1961ac6249
commit 164f752338
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -24,9 +24,7 @@ inspector = config \
w = contentWidth,
h = textInputHeight,
# onChange = text \ batch [config.state.query := text, config.state.focusedIndex := 0],
onChange = text \ batch [],
onKeyDown = key \ key
| _ \ noOp
onChange = text \ batch []
}
) sourceLines
};

@ -3,13 +3,20 @@ import { store } from './runtime-js';
let matchCounter = 0;
type CompileCtx = {
useStore: boolean;
bound: Set<string>;
topLevel: Set<string>;
};
const defaultCtx: CompileCtx = { useStore: true, bound: new Set(), topLevel: new Set() };
export const definitions: Map<string, AST> = new Map();
export const dependencies: Map<string, Set<string>> = new Map();
export const dependents: Map<string, Set<string>> = new Map();
export const astRegistry = new Map<number, AST>();
let astIdCounter = 0;
export function compile(ast: AST, useStore = true, bound = new Set<string>(), topLevel = new Set<string>()): string {
export function compile(ast: AST, ctx: CompileCtx = defaultCtx): string {
switch (ast.kind) {
case 'literal':
if (ast.value.kind === 'string')
@ -19,80 +26,78 @@ export function compile(ast: AST, useStore = true, bound = new Set<string>(), to
throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`);
case 'variable': {
if (bound.has(ast.name)) {
if (ctx.bound.has(ast.name)) {
return sanitizeName(ast.name);
}
return sanitize(ast.name, useStore, topLevel);
return sanitize(ast.name, ctx);
}
case 'lambda': {
const newBound = new Set([...bound, ...ast.params]);
const newBound = new Set([...ctx.bound, ...ast.params]);
const newCtx = { ...ctx, bound: newBound };
const params = ast.params.map(sanitizeName).join(') => (');
const id = astIdCounter++;
astRegistry.set(id, ast);
return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound, topLevel)}, { _astId: (${id}) })`;
return `Object.assign((${params}) => ${compile(ast.body, newCtx)}, { _astId: (${id}) })`;
}
case 'apply':
// Constructor
if (ast.func.kind === 'constructor') {
const ctorName = ast.func.name;
const arg = compile(ast.args[0], useStore, bound, topLevel);
const arg = compile(ast.args[0], ctx);
return `({ _tag: "${ctorName}", _0: ${arg} })`;
}
const args = ast.args.map(a => compile(a, useStore, bound, topLevel)).join(')(');
return `${compile(ast.func, useStore, bound, topLevel)}(${args})`;
const args = ast.args.map(a => compile(a, ctx)).join(')(');
return `${compile(ast.func, ctx)}(${args})`;
case 'record': {
const parts = ast.entries.map(entry =>
entry.kind === 'spread'
? `...${compile(entry.expr, useStore, bound, topLevel)}`
: `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound, topLevel)}`
? `...${compile(entry.expr, ctx)}`
: `${sanitizeName(entry.key)}: ${compile(entry.value, ctx)}`
)
return `({${parts.join(', ')}})`;
}
case 'list': {
const elements = ast.elements.map(e =>
'spread' in e ? `...${compile(e.spread, useStore, bound, topLevel)}` : compile(e, useStore, bound, topLevel)
'spread' in e ? `...${compile(e.spread, ctx)}` : compile(e, ctx)
);
return `[${elements.join(', ')}]`;
}
case 'record-access':
return `${compile(ast.record, useStore, bound, topLevel)}.${sanitizeName(ast.field)}`;
return `${compile(ast.record, ctx)}.${sanitizeName(ast.field)}`;
case 'record-update':
const updates = Object.entries(ast.updates)
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound, topLevel)}`);
return `({...${compile(ast.record, useStore, bound, topLevel)}, ${updates.join(', ')}})`;
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, ctx)}`);
return `({...${compile(ast.record, ctx)}, ${updates.join(', ')}})`;
case 'let':
const newBound = new Set([...bound, ast.name]);
return `(() => { let ${sanitizeName(ast.name)} = ${compile(ast.value, useStore, newBound, topLevel)};
return ${compile(ast.body, useStore, newBound, topLevel)}; })()`;
case 'let': {
const newBound = new Set([...ctx.bound, ast.name]);
const newCtx = { ...ctx, bound: newBound };
return `(() => { let ${sanitizeName(ast.name)} = ${compile(ast.value, newCtx)};
return ${compile(ast.body, newCtx)}; })()`;
}
case 'match':
return compileMatch(ast, useStore, bound, topLevel);
return compileMatch(ast, ctx);
case 'constructor':
return `({ _tag: "${ast.name}" })`;
/*
return `((arg) => arg && typeof arg === 'object' && !arg._tag
? { _tag: "${ast.name}", ...arg }
: { _tag: "${ast.name}", _0: arg })`;
*/
case 'rebind': {
const rootName = getRootName(ast.target);
const path = getPath(ast.target);
const value = compile(ast.value, useStore, bound, topLevel);
const value = compile(ast.value, ctx);
if (!rootName) throw new Error('Rebind target must be a variable');
if (bound.has(rootName)) {
const target = compile(ast.target, useStore, bound, topLevel);
if (ctx.bound.has(rootName)) {
const target = compile(ast.target, ctx);
return `(() => { ${target} = ${value}; return { _tag: "NoOp" }; })()`;
}
@ -109,7 +114,7 @@ export function compile(ast: AST, useStore = true, bound = new Set<string>(), to
}
}
function sanitize(name: string, useStore = true, topLevel: Set<string>): string {
function sanitize(name: string, { useStore, topLevel }: CompileCtx): string {
if (!useStore && topLevel.has(name)) return sanitizeName(name);
return `store[${JSON.stringify(name)}]`
}
@ -128,8 +133,8 @@ function sanitizeName(name: string): string {
return name.replace(/-/g, '_');
}
function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set<string>(), topLevel = new Set<string>()): string {
const expr = compile(ast.expr, useStore, bound, topLevel);
function compileMatch(ast: AST & { kind: 'match'}, ctx: CompileCtx): string {
const expr = compile(ast.expr, ctx);
const tmpVar = `_m${matchCounter++}`;
let code = `((${tmpVar}) => { `;
@ -137,12 +142,13 @@ function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new
for (const c of ast.cases) {
const { condition, bindings } = compilePattern(c.pattern, tmpVar);
const patternBound = patternVars(c.pattern);
const newBound = new Set([...bound, ...patternBound]);
const newBound = new Set([...ctx.bound, ...patternBound]);
const newCtx = { ...ctx, bound: newBound };
code += `if (${condition}) { `;
if (bindings.length > 0) {
code += `const ${bindings.join(', ')}; `;
}
code += `return ${compile(c.result, useStore, newBound, topLevel)}; }`;
code += `return ${compile(c.result, newCtx)}; }`;
}
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
@ -238,7 +244,8 @@ export function compileAndRun(defs: Definition[]) {
}
for (const def of defs) {
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false, new Set(), topLevel)};`;
const ctx: CompileCtx = { useStore: false, topLevel, bound: new Set() };
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, ctx)};`;
compiledDefs.push(compiled);
try {

@ -202,6 +202,22 @@ export class Parser {
return expr;
}
private parseCommaSeparated<T>(closeToken: Token['kind'], parseItem: () => T): T[] {
const items: T[] = [];
let first = true;
while (this.current().kind !== closeToken) {
if (!first) {
this.expect('comma');
if (this.current().kind === closeToken) break; // trailing commas
}
first = false;
items.push(parseItem());
}
return items;
}
private parseMatch(expr: AST): AST {
const token = this.current();
const cases: MatchCase[] = [];
@ -288,23 +304,16 @@ export class Parser {
// Record
if (token.kind === 'open-brace') {
this.advance();
const fields: { [key: string]: Pattern } = {};
let first = true;
while (this.current().kind !== 'close-brace') {
if (!first) {
this.expect('comma');
if (this.current().kind === 'close-brace') break; // trailing commas
}
first = false;
const items = this.parseCommaSeparated('close-brace', () => {
const keyToken = this.expect('ident');
const key = (keyToken as { value: string }).value;
this.expect('equals');
fields[key] = this.parsePattern();
}
return { key, pattern: this.parsePattern() };
});
this.expect('close-brace');
const fields: { [key: string]: Pattern } = {};
for (const item of items) fields[item.key] = item.pattern;
return { kind: 'record', fields };
}
@ -429,23 +438,17 @@ export class Parser {
if (this.current().kind === 'open-brace') {
// Record update
this.advance();
const updates: { [key: string]: AST } = {};
let first = true;
while (this.current().kind !== 'close-brace') {
if (!first) {
this.expect('comma');
if (this.current().kind === 'close-brace') break; // trailing commas
}
first = false;
const items = this.parseCommaSeparated('close-brace', () => {
const keyToken = this.expect('ident');
const key = (keyToken as { value: string }).value;
this.expect('equals');
updates[key] = this.parseExpression();
}
return { key, value: this.parseExpression() };
});
this.expect('close-brace');
const updates: { [key: string]: AST } = {};
for (const item of items) updates[item.key] = item.value;
expr = { kind: 'record-update', record: expr, updates, ...this.getPos(token) }
} else {
@ -475,26 +478,15 @@ export class Parser {
if (token.kind === 'open-bracket') {
this.advance();
const items: AST[] = [];
let first = true;
while (this.current().kind !== 'close-bracket') {
if (!first) {
this.expect('comma');
if (this.current().kind === 'close-bracket') break; // trailing commas
}
first = false;
const items = this.parseCommaSeparated('close-bracket', () => {
// Spread
if (this.current().kind === 'dot-dot-dot') {
const spreadToken = this.current();
this.advance();
const expr = this.parseExpression();
items.push({ kind: 'list-spread', spread: expr, ...this.getPos(spreadToken) })
} else {
items.push(this.parseExpression());
return { kind: 'list-spread' as const, spread: this.parseExpression(), ...this.getPos(spreadToken) };
}
}
return this.parseExpression();
});
this.expect('close-bracket');
return { kind: 'list', elements: items, ...this.getPos(token) };
@ -502,28 +494,16 @@ export class Parser {
if (token.kind === 'open-brace') {
this.advance();
const entries: Array<{ kind: 'field', key: string, value: AST } | { kind: 'spread', expr: AST }> = [];
let first = true;
while (this.current().kind !== 'close-brace') {
if (!first) {
this.expect('comma');
if (this.current().kind === 'close-brace') break; // trailing commas
}
first = false;
const entries = this.parseCommaSeparated('close-brace', () => {
if (this.current().kind === 'dot-dot-dot') {
this.advance();
const expr = this.parseExpression();
entries.push({ kind: 'spread', expr });
} else {
const keyToken = this.expect('ident');
const key = (keyToken as { value: string }).value;
this.expect('equals');
entries.push({ kind: 'field', key, value: this.parseExpression() });
return { kind: 'spread' as const, expr: this.parseExpression() };
}
}
const keyToken = this.expect('ident');
const key = (keyToken as { value: string }).value;
this.expect('equals');
return { kind: 'field' as const, key, value: this.parseExpression() };
});
this.expect('close-brace');
return { kind: 'record', entries, ...this.getPos(token) };

@ -262,16 +262,6 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
handleEvent(hit.onScroll(delta));
}
/*
dispatchToFocused({
_tag: 'Scroll',
_0: {
deltaX: Math.round(e.deltaX),
deltaY: Math.round(e.deltaY)
}
});
*/
e.preventDefault();
rerender();
});

@ -117,7 +117,7 @@ export const _rt = {
const tokens = tokenize(`_tmp = ${code};`);
const parser = new Parser(tokens, "");
const defs = parser.parse();
recompile(name, defs[0]. body);
recompile(name, defs[0].body);
return { _tag: 'Ok' };
},
undefine: (name: string) => {

@ -249,29 +249,27 @@ export function _measure(ui: UIValue): { width: number, height: number } {
}
}
export function hitTest(x: number, y: number): { onClick: any, relativeX: number, relativeY: number } | null {
for (let i = clickRegions.length - 1; i >= 0; i--) {
const region = clickRegions[i];
function findRegion<T extends { x: number, y: number, width: number, height: number }>(regions: T[], x: number, y: number ): T | null {
for (let i = regions.length - 1; i >= 0; i--) {
const r = regions[i];
if (x >= region.x && x < region.x + region.width &&
y >= region.y && y < region.y + region.height) {
return {
onClick: region.onClick,
relativeX: x - region.x,
relativeY: y - region.y,
};
if (x >= r.x && x < r.x + r.width &&
y >= r.y && y < r.y + r.height) {
return r;
}
}
return null;
}
export function hitTest(x: number, y: number): { onClick: any, relativeX: number, relativeY: number } | null {
const region = findRegion(clickRegions, x, y);
if (!region) return null;
return { onClick: region.onClick, relativeX: x - region.x, relativeY: y - region.y };
}
export function scrollHitTest(x: number, y: number): { onScroll: any } | null {
for (let i = scrollRegions.length - 1; i >= 0; i--) {
const region = scrollRegions[i];
if (x >= region.x && x < region.x + region.width &&
y >= region.y && y < region.y + region.height) {
return { onScroll: region.onScroll };
}
}
return null;
const region = findRegion(scrollRegions, x, y);
if (!region) return null;
return { onScroll: region.onScroll };
}

Loading…
Cancel
Save