Cleaning
This commit is contained in:
parent
1961ac6249
commit
164f752338
6 changed files with 95 additions and 122 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
34
src/ui.ts
34
src/ui.ts
|
|
@ -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…
Add table
Add a link
Reference in a new issue