diff --git a/client/space_lua/aggregates.test.ts b/client/space_lua/aggregates.test.ts index 5b15288a..ed96cee5 100644 --- a/client/space_lua/aggregates.test.ts +++ b/client/space_lua/aggregates.test.ts @@ -1602,7 +1602,7 @@ test("applyQuery: group by + select with aggregates", async () => { data, { objectVariable: "p", - groupBy: [parseExpressionString("p.dept")], + groupBy: [{ expr: parseExpressionString("p.dept") }], select: parseExpressionString( "{ dept = key, total = sum(p.salary), n = count(p.salary) }", ), @@ -1633,7 +1633,7 @@ test("applyQuery: group by + having with aggregate", async () => { data, { objectVariable: "p", - groupBy: [parseExpressionString("p.dept")], + groupBy: [{ expr: parseExpressionString("p.dept") }], having: parseExpressionString("count(p.name) > 1"), }, new LuaEnv(), @@ -1655,7 +1655,7 @@ test("applyQuery: group by without aggregates still works", async () => { data, { objectVariable: "p", - groupBy: [parseExpressionString("p.dept")], + groupBy: [{ expr: parseExpressionString("p.dept") }], select: parseExpressionString("key"), }, new LuaEnv(), @@ -1678,7 +1678,7 @@ test("applyQuery: having with compound expression", async () => { data, { objectVariable: "p", - groupBy: [parseExpressionString("p.dept")], + groupBy: [{ expr: parseExpressionString("p.dept") }], having: parseExpressionString("sum(p.salary) > 100"), select: parseExpressionString("{ dept = key, total = sum(p.salary) }"), }, @@ -1703,7 +1703,7 @@ test("applyQuery: implicit _ with group by + multiple aggregates", async () => { const results = await applyQuery( data, { - groupBy: [parseExpressionString("_.category")], + groupBy: [{ expr: parseExpressionString("_.category") }], select: parseExpressionString( "{ cat = key, total = sum(_.price), best = max(_.price) }", ), @@ -1737,7 +1737,7 @@ test("applyQuery: group by + having + order by + limit", async () => { data, { objectVariable: "t", - groupBy: [parseExpressionString("t.tag")], + groupBy: [{ expr: parseExpressionString("t.tag") }], having: parseExpressionString("count(t.page) > 1"), orderBy: [{ expr: parseExpressionString("key"), desc: false }], select: parseExpressionString("key"), @@ -1761,7 +1761,7 @@ test("applyQuery: select with aggregate division (float result)", async () => { data, { objectVariable: "p", - groupBy: [parseExpressionString("p.dept")], + groupBy: [{ expr: parseExpressionString("p.dept") }], select: parseExpressionString( "{ dept = key, avg_salary = sum(p.salary) / count(p.salary) }", ), diff --git a/client/space_lua/ast.ts b/client/space_lua/ast.ts index 3024476f..f5f35784 100644 --- a/client/space_lua/ast.ts +++ b/client/space_lua/ast.ts @@ -311,10 +311,10 @@ export type LuaQueryClause = | LuaGroupByClause | LuaHavingClause; +// Field list used by `from`, `select` and `group by` clauses export type LuaFromClause = { type: "From"; - name?: string; - expression: LuaExpression; + fields: LuaTableField[]; } & ASTContext; export type LuaWhereClause = { @@ -348,12 +348,12 @@ export type LuaOrderBy = { export type LuaSelectClause = { type: "Select"; - expression: LuaExpression; + fields: LuaTableField[]; } & ASTContext; export type LuaGroupByClause = { type: "GroupBy"; - expressions: LuaExpression[]; + fields: LuaTableField[]; } & ASTContext; export type LuaHavingClause = { diff --git a/client/space_lua/eval.ts b/client/space_lua/eval.ts index 22037209..762f95a7 100644 --- a/client/space_lua/eval.ts +++ b/client/space_lua/eval.ts @@ -4,6 +4,7 @@ import type { LuaExpression, LuaLValue, LuaStatement, + LuaTableField, NumericType, } from "./ast.ts"; import { LuaAttribute } from "./ast.ts"; @@ -38,7 +39,11 @@ import { luaValueToJS, singleResult, } from "./runtime.ts"; -import { type LuaCollectionQuery, toCollection } from "./query_collection.ts"; +import { + type LuaCollectionQuery, + type LuaGroupByEntry, + toCollection, +} from "./query_collection.ts"; import { coerceNumericPair, coerceToNumber, @@ -255,8 +260,10 @@ export function luaOp( case "+": { // Ultra-fast path: both plain numbers with no float type annotation (int + int) if ( - typeof left === "number" && typeof right === "number" && - leftType !== "float" && rightType !== "float" + typeof left === "number" && + typeof right === "number" && + leftType !== "float" && + rightType !== "float" ) { return left + right; } @@ -264,8 +271,10 @@ export function luaOp( } case "-": { if ( - typeof left === "number" && typeof right === "number" && - leftType !== "float" && rightType !== "float" + typeof left === "number" && + typeof right === "number" && + leftType !== "float" && + rightType !== "float" ) { const r = left - right; // Integer subtraction can produce -0 (e.g. 0 - 0), normalize to +0 @@ -275,8 +284,10 @@ export function luaOp( } case "*": { if ( - typeof left === "number" && typeof right === "number" && - leftType !== "float" && rightType !== "float" + typeof left === "number" && + typeof right === "number" && + leftType !== "float" && + rightType !== "float" ) { const r = left * right; // Integer multiplication can produce -0 (e.g. 0 * -1), normalize to +0 @@ -286,7 +297,15 @@ export function luaOp( } case "/": case "^": { - return luaArithGeneric(op as NumericArithOp, left, right, leftType, rightType, ctx, sf); + return luaArithGeneric( + op as NumericArithOp, + left, + right, + leftType, + rightType, + ctx, + sf, + ); } case "..": { // Fast path: string .. string (most common in SilverBullet — key building, templates) @@ -362,19 +381,25 @@ export function luaOp( return luaRelWithMetamethod("<", left, right, ctx, sf); } case "<=": { - if (typeof left === "number" && typeof right === "number") return left <= right; - if (typeof left === "string" && typeof right === "string") return left <= right; + if (typeof left === "number" && typeof right === "number") + return left <= right; + if (typeof left === "string" && typeof right === "string") + return left <= right; return luaRelWithMetamethod("<=", left, right, ctx, sf); } // Lua: `a>b` is `b=b` is `b<=a` case ">": { - if (typeof left === "number" && typeof right === "number") return left > right; - if (typeof left === "string" && typeof right === "string") return left > right; + if (typeof left === "number" && typeof right === "number") + return left > right; + if (typeof left === "string" && typeof right === "string") + return left > right; return luaRelWithMetamethod("<", right, left, ctx, sf); } case ">=": { - if (typeof left === "number" && typeof right === "number") return left >= right; - if (typeof left === "string" && typeof right === "string") return left >= right; + if (typeof left === "number" && typeof right === "number") + return left >= right; + if (typeof left === "string" && typeof right === "string") + return left >= right; return luaRelWithMetamethod("<=", right, left, ctx, sf); } } @@ -669,6 +694,168 @@ const operatorsMetaMethods: Record< }, }; +function deriveFieldName(e: LuaExpression): string | undefined { + switch (e.type) { + case "Variable": + return e.name; + case "PropertyAccess": + return e.property; + case "FunctionCall": + if (e.name) return e.name; + if (e.prefix.type === "Variable") return e.prefix.name; + if (e.prefix.type === "PropertyAccess") return e.prefix.property; + return undefined; + case "FilteredCall": + return deriveFieldName(e.call); + case "AggregateCall": + return deriveFieldName((e as any).call); + default: + return undefined; + } +} + +function fieldsToExpression( + fields: LuaTableField[], + ctx: ASTCtx, +): LuaExpression { + if (fields.length === 1 && fields[0].type === "ExpressionField") { + return fields[0].value; + } + const promoted: LuaTableField[] = fields.map((f) => { + if (f.type !== "ExpressionField") return f; + const key = deriveFieldName(f.value); + if (key) { + return { + type: "PropField", + key, + value: f.value, + ctx: f.ctx, + } as LuaTableField; + } + return f; + }); + return { type: "TableConstructor", fields: promoted, ctx }; +} + +function fieldsToGroupByEntries(fields: LuaTableField[]): LuaGroupByEntry[] { + return fields.map((f) => { + switch (f.type) { + case "PropField": + return { expr: f.value, alias: f.key }; + case "ExpressionField": + return { expr: f.value }; + case "DynamicField": + return { expr: f.value }; + } + }); +} + +type FromSource = + | { kind: "single"; objectVariable?: string; expression: LuaExpression } + | { kind: "cross"; sources: { name: string; expression: LuaExpression }[] }; + +function fromFieldsToSource(fields: LuaTableField[], ctx: ASTCtx): FromSource { + if (fields.length === 1) { + const f = fields[0]; + if (f.type === "ExpressionField") { + return { kind: "single", expression: f.value }; + } + if (f.type === "PropField") { + return { kind: "single", objectVariable: f.key, expression: f.value }; + } + } + + const sources: { name: string; expression: LuaExpression }[] = []; + for (const f of fields) { + if (f.type !== "PropField") { + throw new LuaRuntimeError("Multi-source 'from' requires named sources", { + ref: ctx, + } as any); + } + sources.push({ name: f.key, expression: f.value }); + } + return { kind: "cross", sources }; +} + +async function normalizeToArray( + collection: LuaValue, + sf: LuaStackFrame, +): Promise { + if (collection instanceof LuaTable && collection.empty()) { + return []; + } + if (collection instanceof LuaTable) { + if (collection.length > 0) { + const arr: any[] = []; + for (let i = 1; i <= collection.length; i++) { + arr.push(collection.rawGet(i)); + } + return arr; + } + return [collection]; + } + if (Array.isArray(collection)) { + return collection; + } + if ( + typeof collection === "object" && + collection !== null && + "query" in collection && + typeof (collection as any).query === "function" + ) { + const allItems = await (collection as any).query( + { distinct: false }, + new LuaEnv(), + sf, + ); + return Array.isArray(allItems) ? allItems : [allItems]; + } + const jsVal = luaValueToJS(collection, sf); + return Array.isArray(jsVal) ? jsVal : [jsVal]; +} + +async function evalCrossJoinSources( + sources: { name: string; expression: LuaExpression }[], + env: LuaEnv, + sf: LuaStackFrame, + ctx: ASTCtx, +): Promise { + // Evaluate each source and normalize to arrays + const arrays: { name: string; items: any[] }[] = []; + for (const src of sources) { + const val = await evalExpression(src.expression, env, sf); + if (val === null || val === undefined) { + throw new LuaRuntimeError( + `Cross-join source '${src.name}' is nil`, + sf.withCtx(ctx), + ); + } + const items = await normalizeToArray(val, sf); + arrays.push({ name: src.name, items }); + } + + // Cartesian product + let product: Record[] = [{}]; + for (const { name, items } of arrays) { + const newProduct: Record[] = []; + for (const combo of product) { + for (const item of items) { + newProduct.push({ ...combo, [name]: item }); + } + } + product = newProduct; + } + + // Convert each combination to a `LuaTable` row + return product.map((combo) => { + const row = new LuaTable(); + for (const key in combo) { + void row.rawSet(key, combo[key]); + } + return row; + }); +} + export function evalExpression( e: LuaExpression, env: LuaEnv, @@ -915,8 +1102,89 @@ export function evalExpression( if (!findFromClause) { throw new LuaRuntimeError("No from clause found", sf.withCtx(q.ctx)); } - const objectVariable = findFromClause.name; - const objectExpression = findFromClause.expression; + const fromSource = fromFieldsToSource( + findFromClause.fields, + findFromClause.ctx, + ); + + if (fromSource.kind === "cross") { + // Materialize Cartesian product, then query + return (async () => { + const rows = await evalCrossJoinSources( + fromSource.sources, + env, + sf, + findFromClause.ctx, + ); + const collection: any = toCollection(rows); + + // Build up query object + const query: LuaCollectionQuery = { + objectVariable: undefined, + distinct: true, + }; + + // Map clauses to query parameters + for (const clause of q.clauses) { + switch (clause.type) { + case "Where": { + query.where = clause.expression; + break; + } + case "OrderBy": { + query.orderBy = clause.orderBy.map((o) => ({ + expr: o.expression, + desc: o.direction === "desc", + nulls: o.nulls, + using: o.using, + })); + break; + } + case "Select": { + query.select = fieldsToExpression(clause.fields, clause.ctx); + break; + } + case "Limit": { + const limitVal = await evalExpression(clause.limit, env, sf); + query.limit = Number(limitVal); + if (clause.offset) { + const offsetVal = await evalExpression( + clause.offset, + env, + sf, + ); + query.offset = Number(offsetVal); + } + break; + } + case "Offset": { + const offsetVal = await evalExpression( + clause.offset, + env, + sf, + ); + query.offset = Number(offsetVal); + break; + } + case "GroupBy": { + query.groupBy = fieldsToGroupByEntries(clause.fields); + break; + } + case "Having": { + query.having = clause.expression; + break; + } + } + } + + return (collection as any) + .query(query, env, sf, globalThis.client?.config) + .then(jsToLuaValue); + })(); + } + + // Single-source + const { objectVariable, expression: objectExpression } = fromSource; return Promise.resolve(evalExpression(objectExpression, env, sf)).then( async (collection: LuaValue) => { if (!collection) { @@ -974,7 +1242,7 @@ export function evalExpression( break; } case "Select": { - query.select = clause.expression; + query.select = fieldsToExpression(clause.fields, clause.ctx); break; } case "Limit": { @@ -1000,7 +1268,7 @@ export function evalExpression( break; } case "GroupBy": { - query.groupBy = clause.expressions; + query.groupBy = fieldsToGroupByEntries(clause.fields); break; } case "Having": { @@ -1139,13 +1407,19 @@ function evalPrefixExpression( const argsVal = evalExpressions(fc.args, env, sf); if (!isPromise(argsVal)) { - const allArgs = selfArgs.length > 0 - ? [...selfArgs, ...(argsVal as LuaValue[])] - : argsVal as LuaValue[]; + const allArgs = + selfArgs.length > 0 + ? [...selfArgs, ...(argsVal as LuaValue[])] + : (argsVal as LuaValue[]); return luaCall(calleeVal, allArgs, fc.ctx, sf); } return (argsVal as Promise).then((args) => - luaCall(calleeVal, selfArgs.length > 0 ? [...selfArgs, ...args] : args, fc.ctx, sf), + luaCall( + calleeVal, + selfArgs.length > 0 ? [...selfArgs, ...args] : args, + fc.ctx, + sf, + ), ); }; @@ -1319,17 +1593,41 @@ function evalBinaryWithLR( ); } return (rightVal as Promise).then((rv) => - luaOp(op, singleResult(leftVal), singleResult(rv), leftType, rightType, ctx, sf), + luaOp( + op, + singleResult(leftVal), + singleResult(rv), + leftType, + rightType, + ctx, + sf, + ), ); } return (leftVal as Promise).then((lv) => { const rightVal = evalExpression(rightExpr, env, sf); if (!isPromise(rightVal)) { - return luaOp(op, singleResult(lv), singleResult(rightVal), leftType, rightType, ctx, sf); + return luaOp( + op, + singleResult(lv), + singleResult(rightVal), + leftType, + rightType, + ctx, + sf, + ); } return (rightVal as Promise).then((rv) => - luaOp(op, singleResult(lv), singleResult(rv), leftType, rightType, ctx, sf), + luaOp( + op, + singleResult(lv), + singleResult(rv), + leftType, + rightType, + ctx, + sf, + ), ); }); } @@ -1975,7 +2273,10 @@ export function evalStatement( const w = asWhile(s); // Sync-first loop that re-enters sync mode after each async iteration - const runSyncFirst = (): undefined | ControlSignal | Promise => { + const runSyncFirst = (): + | undefined + | ControlSignal + | Promise => { while (true) { const c = evalExpression(w.condition, env, sf); if (isPromise(c)) { @@ -2016,7 +2317,10 @@ export function evalStatement( const rep = asRepeat(s); // Sync-first loop that re-enters sync mode after each async iteration - const runSyncFirst = (): undefined | ControlSignal | Promise => { + const runSyncFirst = (): + | undefined + | ControlSignal + | Promise => { while (true) { const rr = evalStatement(rep.block, env, sf, returnOnReturn); if (isPromise(rr)) { @@ -2024,9 +2328,8 @@ export function evalStatement( if (res !== undefined) { return isBreakSignal(res) ? undefined : res; } - return rpThen( - evalExpression(rep.condition, env, sf), - (cv) => luaTruthy(cv) ? undefined : runSyncFirst(), + return rpThen(evalExpression(rep.condition, env, sf), (cv) => + luaTruthy(cv) ? undefined : runSyncFirst(), ); }); } @@ -2369,12 +2672,7 @@ export function evalStatement( const localEnv = makeIterEnv(); setIterVars(localEnv, fi.names, iterResult.values); - const r = evalStatement( - fi.block, - localEnv, - sf, - returnOnReturn, - ); + const r = evalStatement(fi.block, localEnv, sf, returnOnReturn); return rpThen(r, (res) => { if (res !== undefined) { if (isBreakSignal(res)) { diff --git a/client/space_lua/lua.grammar b/client/space_lua/lua.grammar index 32d8e20c..e0c4b9fe 100644 --- a/client/space_lua/lua.grammar +++ b/client/space_lua/lua.grammar @@ -73,6 +73,8 @@ AttNameList { list } AttName { Name Attrib } Attrib { ( "<" Name ">" )? } +FieldList { field ("," field)* } + exp { kw<"nil"> | kw<"true"> | kw<"false"> | "..." | Number | @@ -101,14 +103,14 @@ QueryClause { HavingClause } -FromClause { ckw<"from"> (Name "=")? exp } +FromClause { ckw<"from"> FieldList } WhereClause { ckw<"where"> exp } LimitClause { ckw<"limit"> exp ("," exp)? } OffsetClause { ckw<"offset"> exp } OrderByClause { ckw<"order"> ckw<"by"> list } OrderBy { exp (ckw<"desc"> | ckw<"asc"> | kw<"using"> (Name | kw<"function"> FuncBody))? (ckw<"nulls"> (ckw<"first"> | ckw<"last">))? } -SelectClause { ckw<"select"> exp } -GroupByClause { ckw<"group"> ckw<"by"> list } +SelectClause { ckw<"select"> FieldList } +GroupByClause { ckw<"group"> ckw<"by"> FieldList } HavingClause { ckw<"having"> exp } AggOrderBy { ckw<"order"> ckw<"by"> list } diff --git a/client/space_lua/parse-lua.js b/client/space_lua/parse-lua.js index 76cc469b..4d33576e 100644 --- a/client/space_lua/parse-lua.js +++ b/client/space_lua/parse-lua.js @@ -1,90 +1,22 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -import { LRParser } from "@lezer/lr"; -const spec_identifier = { - __proto__: null, - break: 16, - goto: 20, - do: 24, - end: 26, - while: 30, - nil: 32, - true: 34, - false: 36, - order: 81, - by: 83, - desc: 87, - asc: 89, - using: 90, - function: 92, - nulls: 99, - first: 101, - last: 103, - or: 106, - and: 108, - not: 130, - query: 142, - from: 149, - where: 153, - select: 159, - limit: 163, - offset: 167, - group: 171, - having: 175, - filter: 179, - repeat: 182, - until: 184, - if: 188, - then: 190, - elseif: 192, - else: 194, - for: 198, - in: 206, - local: 216, - return: 232, -}; +import {LRParser} from "@lezer/lr" +const spec_identifier = {__proto__:null,break:16, goto:20, do:24, end:26, while:30, nil:32, true:34, false:36, order:81, by:83, desc:87, asc:89, using:90, function:92, nulls:99, first:101, last:103, or:106, and:108, not:130, query:142, from:149, where:155, select:161, limit:165, offset:169, group:173, having:177, filter:181, repeat:184, until:186, if:190, then:192, elseif:194, else:196, for:200, in:208, local:218, return:234} export const parser = LRParser.deserialize({ version: 14, - states: - "KUO!ZQPOOOOQO'#Cc'#CcO!UQPO'#CaO!bQPOOOOQO'#FZ'#FZO!vQQO'#CwO$]QPO'#FYOOQO'#FY'#FYO$gQPO'#FYOOQO'#En'#EnO%vQPO'#EmOOQO'#FU'#FUOOQO'#Eu'#EuO%{QPO'#C_OOQO'#C_'#C_QOQPOOO!UQPO'#CeO&`QPO'#CgO!vQQO'#CjO&gQPO'#EYO!vQQO'#E]O!UQPO'#EbO!UQPO'#EiO&nQPO'#EoO&vQQO'#EsO'^QPO,58{OOQO'#Cq'#CqO!UQPO,59^O!vQQO,59`O(gQQO'#C|O(nQQO'#F`OOQO'#F['#F[OOQO,59f,59fO!UQPO,59fO(xQPO'#FWO,wQPO,59cO-OQPO'#FYOOQO'#Dp'#DpOOQO'#Dq'#DqOOQO'#Dr'#DrO!vQQO'#DnOOQO'#FW'#FWO/|QPO'#DsO0RQSO'#DtO0WQPO'#FOO0`QPO,5<`O!vQQO,5;XOOQO-E8s-E8sOOQO,58y,58yOOQO,59P,59PO0hQPO,59RO0mQPO,59UO0tQPO,5:tO0yQPO,5:wO1QQPO'#FsOOQO'#Ef'#EfO1]QPO'#EeO1bQPO,5:|O1gQPO'#EjO/|QPO,5;TO1rQQO'#EqO3YQPO'#FuOOQO'#Ep'#EpO!UQPO,5;VO4mQPO,5;ZO5}QPO'#FaOOQO'#Eh'#EhOOQO,5;_,5;_O7gQPO,5;_OOQO1G.g1G.gOOQO1G.x1G.xO7{QPO1G.zO8SQWO'#FWO;cQWO'#FZO!vQQO'#DOOPQPO,5;}O&`QPO1G/cOOQO-E8x-E8xOOQO7+%f7+%fO!>XQWO,5:cO!>{QSO,5:cO!?lQSO,5:eO!-sQQO,5:gO!@]QSO,5:hO!@|Q`O'#FWO!COQ`O'#FYO!FXQ`O,5:jO!F{QSO,5:lO!-sQQO,5:nO!GlQSO,5:pOOQO<OAN>OOGnQQO'#DnO!-sQQO'#DnOGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|OGnQQO,59|O!-sQQO,59|O!-sQQO'#EwO#/vQPO,59rO!-sQQO'#EyO/|QPO1G/^", - stateData: - "#0R~O#vOS#wOSPOS~OSZOUQOWZOY`O[aO_bOlTO!OfO!}cO#QdO#VeO#`gO#hhO#yPO~O#tRP~P]OgkOilOlnOoqOqmO#{jO~O`yOayObyOcyOdyOlTOqmO!OzO!cxO!i{O#yPO#{jO$YvO$^uO$dwO~Og#|Xi#|Xl#|Xo#|Xq#|X#{#|X~Ov|O$Q$hX~P#wOS#xXU#xXW#xXY#xX[#xX_#xXl#xX!O#xX!}#xX#Q#xX#V#xX#`#xX#h#xX#t#xX#y#xX]#xX#O#xX#S#xX#T#xX~P#wO$Q!OO~O#tRX]RX#ORX#SRX#TRX~P]O]RP~P]O#ORP~P]O!O!aO#yPO~OS!eO#t#gX]#gX#O#gX#S#gX#T#gX~P!vOU!gO~O`yOayObyOcyOdyOi!lOlTOqmO!OzO!c'gO!i{O#yPO#{jO$YvO$^uO$dwO~Ou!oO~P'cOm!rOx!uO~P!vOm#zX!V#zX!W#zX!X#zX!^#zX$W#zX$X#zX$Y#zX$Z#zX$[#zX$]#zX$^#zX$_#zX$`#zX$a#zX$b#zX$c#zX[#zX#R#zXS#zXv#zX#t#zXj#zXx#zXU#zXW#zXY#zX_#zX!O#zX!}#zX#Q#zX#V#zX#`#zX#h#zX#y#zX]#zX#O#zX{#zX|#zX}#zX!R#zX#S#zX#T#zX~P!bO!V#WO!W#XO!X#PO!^#TO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OO~Om!wO~P+sO!{#YOm#|X!V#|X!W#|X!X#|X!^#|X$W#|X$X#|X$Y#|X$Z#|X$[#|X$]#|X$^#|X$_#|X$`#|X$a#|X$b#|X$c#|X[#|X#R#|XS#|Xv#|X#t#|Xj#|Xx#|XU#|XW#|XY#|X_#|X!O#|X!}#|X#Q#|X#V#|X#`#|X#h#|X#y#|X]#|X#O#|X{#|X|#|X}#|X!R#|X#S#|X#T#|X~P#wOl#[O~O$e#^O~OlTO#yPO~Ov|O$Q$ha~O]#bO~O[#cO~P+sO#O#dO~O#R#eO~P+sOv#gO$Q#fO#Z$gX~O#Z#iO~O[#jO~Og#kOo#mOl#^X~O$j#oOS#fPU#fPW#fPY#fP[#fP_#fPl#fPv#fP!O#fP!}#fP#Q#fP#V#fP#`#fP#h#fP#t#fP#y#fP$Q#fP]#fP#O#fP#S#fP#T#fP~Ov#qOS$iXU$iXW$iXY$iX[$iX_$iXl$iX!O$iX!}$iX#Q$iX#V$iX#`$iX#h$iX#t$iX#y$iX$Q$iX]$iX#O$iX#S$iX#T$iX~O$Q#tOS#caU#caW#caY#ca[#ca_#cal#ca!O#ca!}#ca#Q#ca#V#ca#`#ca#h#ca#t#ca#y#ca]#ca#O#ca#S#ca#T#ca~Ov#uOS$TX#t$TXm$TXx$TXU$TXW$TXY$TX[$TX_$TXl$TX!O$TX!}$TX#Q$TX#V$TX#`$TX#h$TX#y$TX]$TX#O$TX#S$TX#T$TX~P+sOS#wO#t#ga]#ga#O#ga#S#ga#T#ga~Oj#xO~P+sOu#zX!V#zX!W#zX!X#zX!^#zX$R#zX$W#zX$X#zX$Y#zX$Z#zX$[#zX$]#zX$^#zX$_#zX$`#zX$a#zX$b#zX$c#zXx#zX!l#zX!n#zX!q#zX!s#zX!u#zX!w#zX!y#zX$f#zX~P!bOg#}Xi#}Xl#}Xo#}Xq#}X!V#}X!W#}X!X#}X!^#}X#{#}X$W#}X$X#}X$Y#}X$Z#}X$[#}X$]#}X$^#}X$_#}X$`#}X$a#}X$b#}X$c#}X~O$Q#yOu#}X$R#}X~P9{O!V'wO!W'yO!X'iO!^'qO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OO~OutX$RtX~P;pOu#}O$R#{O~O!{#YOu#|X!V#|X!W#|X!X#|X!^#|X$R#|X$W#|X$X#|X$Y#|X$Z#|X$[#|X$]#|X$^#|X$_#|X$`#|X$a#|X$b#|X$c#|Xx#|X!l#|X!n#|X!q#|X!s#|X!u#|X!w#|X!y#|X$f#|X~P#wOm$PO~Om$POx!uO~Oy$RO~OlnOqmO#{jO~Ol$^O~O$c#OO!V!ba!W!ba!X!ba!^!ba$W!ba$X!ba$Y!ba$Z!ba$[!ba$]!ba$^!ba$_!ba$`!ba$a!ba$b!bav!bax!ba{!ba|!ba}!ba!R!ba~Om!ba[!ba#R!baS!ba#t!baj!baU!baW!baY!ba_!bal!ba!O!ba!}!ba#Q!ba#V!ba#`!ba#h!ba#y!ba]!ba#O!ba#S!ba#T!ba~P?vOc$`OlTO#yPOm!QP~Ox$hO!l$fO!n$gO!q$iO!s$jO!u$kO!w$lO!y$mO~O$f$nO~PCUOv#ra$Q#ra~P#wO]RP#SRP#TRP~P]Ov#gO#Z$ga~Og#kOo$yOl#^a~Ov#qOS$iaU$iaW$iaY$ia[$ia_$ial$ia!O$ia!}$ia#Q$ia#V$ia#`$ia#h$ia#t$ia#y$ia$Q$ia]$ia#O$ia#S$ia#T$ia~Ov#uOS$Ta#t$Tam$Tax$TaU$TaW$TaY$Ta[$Ta_$Tal$Ta!O$Ta!}$Ta#Q$Ta#V$Ta#`$Ta#h$Ta#y$Ta]$Ta#O$Ta#S$Ta#T$Ta~O`yOayObyOcyOdyOlTOqmO!OzO!c'gO!i{O#yPO#{jO$YvO$^uO$dwO~Oj%TO~P+sOu%`O~P'cOu%`O$R%aO~O$c#OO!V!ba!W!ba!X!ba!^!ba$W!ba$X!ba$Y!ba$Z!ba$[!ba$]!ba$^!ba$_!ba$`!ba$a!ba$b!bax!ba!l!ba!n!ba!q!ba!s!ba!u!ba!w!ba!y!ba$f!ba~Ou!ba$R!ba~PIVOm%cO~O!^#TO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOm!Ui!V!Ui!W!Ui!X!Ui[!Ui#R!UiS!Uiv!Ui#t!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui!}!Ui#Q!Ui#V!Ui#`!Ui#h!Ui#y!Ui]!Ui#O!Ui{!Ui|!Ui}!Ui!R!Ui#S!Ui#T!Ui~O$W!xO~PKVO$W!Ui~PKVO!^#TO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOm!Ui!V!Ui!W!Ui!X!Ui$W!Ui$Y!Ui[!Ui#R!UiS!Uiv!Ui#t!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui!}!Ui#Q!Ui#V!Ui#`!Ui#h!Ui#y!Ui]!Ui#O!Ui{!Ui|!Ui}!Ui!R!Ui#S!Ui#T!Ui~O$X!Ui~PN^O$X!yO~PN^O$_!}O$`!}O$a!}O$b!}O$c#OOm!Ui!V!Ui!W!Ui!X!Ui$W!Ui$X!Ui$Y!Ui$Z!Ui$[!Ui[!Ui#R!UiS!Uiv!Ui#t!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui!}!Ui#Q!Ui#V!Ui#`!Ui#h!Ui#y!Ui]!Ui#O!Ui{!Ui|!Ui}!Ui!R!Ui#S!Ui#T!Ui~O!^#TO$]!|O$^!|O~P!#eO!^!Ui$]!Ui$^!Ui~P!#eO$c#OOm!Ui!V!Ui!W!Ui[!Ui#R!UiS!Uiv!Ui#t!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui!}!Ui#Q!Ui#V!Ui#`!Ui#h!Ui#y!Ui]!Ui#O!Ui{!Ui|!Ui}!Ui!R!Ui#S!Ui#T!Ui~O!X!Ui!^!Ui$W!Ui$X!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Ui$_!Ui$`!Ui$a!Ui$b!Ui~P!&rO!X#PO!^#TO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOm!Ui!V!Ui[!Ui#R!UiS!Uiv!Ui#t!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui!}!Ui#Q!Ui#V!Ui#`!Ui#h!Ui#y!Ui]!Ui#O!Ui{!Ui|!Ui}!Ui!R!Ui#S!Ui#T!Ui~O!W#XO~P!)rO!W!Ui~P!)rO!n%gO~Ov%hOm$VX~P#wOv%hOm$VX~Om%jO~O$f%lO~PCUOy%pO~O`yOayObyOcyOdyOlTOqmO!OzO!c'hO!i{O#yPO#{jO$YvO$^uO$dwO~Oy%vO~O]%xO~OS!|qU!|qW!|qY!|q[!|q_!|ql!|q!O!|q!}!|q#Q!|q#V!|q#`!|q#h!|q#t!|q#y!|q]!|q#O!|q#S!|q#T!|q~P+sO]%{O#S%zO#T%|O~Ov%}O~P+sO]&OO~O$k&QO~OS#kav#ka#t#kam#kax#kaU#kaW#kaY#ka[#ka_#kal#ka!O#ka!}#ka#Q#ka#V#ka#`#ka#h#ka#y#ka]#ka#O#ka#S#ka#T#ka~P+sOusi$Rsi~P;pO$Q&RO~O!^'qO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOu!Ui!V!Ui!W!Ui!X!Ui$R!Uix!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui~O$W!xO~P!2tO$W!Ui~P!2tO!^'qO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOu!Ui!V!Ui!W!Ui!X!Ui$R!Ui$W!Ui$Y!Uix!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui~O$X!Ui~P!4vO$X!yO~P!4vO$_!}O$`!}O$a!}O$b!}O$c#OOu!Ui!V!Ui!W!Ui!X!Ui$R!Ui$W!Ui$X!Ui$Y!Ui$Z!Ui$[!Uix!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui~O!^'qO$]!|O$^!|O~P!6xO!^!Ui$]!Ui$^!Ui~P!6xO$c#OO!V!Ui!W!Ui!X!Ui!^!Ui$W!Ui$X!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Ui$_!Ui$`!Ui$a!Ui$b!Uix!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui~Ou!Ui$R!Ui~P!9QO!X'iO!^'qO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOu!Ui!V!Ui$R!Uix!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui~O!W'yO~P!:{O!W!Ui~P!:{Ou&SO~P'cO{&VO|&VO}&WO!R&UOmzXvzX~P+sOv&XOm$UX~Oc&]OlTO#yPO~Ov%hOm$Va~O$Q&`Ox#}X!l#}X!n#}X!q#}X!s#}X!u#}X!w#}X!y#}X$f#}X~P9{Ox!ka!l!ka!n!ka!q!ka!s!ka!u!ka!w!ka!y!ka$f!ka~P;pOx!ma!l!ma!n!ma!q!ma!s!ma!u!ma!w!ma!y!ma$f!ma~P;pOx!pa!l!pa!n!pa!q!pa!s!pa!u!pa!w!pa!y!pa$f!pa~P;pOv#zXx#zX!V#zX!W#zX!X#zX!^#zX!l#zX!n#zX!q#zX!s#zX!u#zX!w#zX!y#zX$W#zX$X#zX$Y#zX$Z#zX$[#zX$]#zX$^#zX$_#zX$`#zX$a#zX$b#zX$c#zX$f#zX{#zX|#zX}#zX!R#zX~P!bO!{#YOv#|Xx#|X!V#|X!W#|X!X#|X!^#|X!l#|X!n#|X!q#|X!s#|X!u#|X!w#|X!y#|X$W#|X$X#|X$Y#|X$Z#|X$[#|X$]#|X$^#|X$_#|X$`#|X$a#|X$b#|X$c#|X$f#|X{#|X|#|X}#|X!R#|X~P#wO!V'xO!W'zO!X'jO!^'rO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OO~Ov&dOx!ra!l!ra!n!ra!q!ra!s!ra!u!ra!w!ra!y!ra$f!ra~P!ETOx!ta!l!ta!n!ta!q!ta!s!ta!u!ta!w!ta!y!ta$f!ta~P;pOx!xa!l!xa!n!xa!q!xa!s!xa!u!xa!w!xa!y!xa$f!xa~P;pO]&iO#S%zO#T&jO~O!S&oO!T&oO~O!R&pOmzavza~O!O&rO#yPO~Ov&XOm$Ua~Om&uO~P+sOm#lav#la~P#wO]&vO~O{&xO|&xO}'|O!R&UOvzXxzX!lzX!nzX!qzX!szX!uzX!wzX!yzX$fzX~P!ETOv'}Ox$UX!l$UX!n$UX!q$UX!s$UX!u$UX!w$UX!y$UX$f$UX~Ov!ba{!ba|!ba}!ba!R!ba~PIVOv'{Ox$TX!l$TX!n$TX!q$TX!s$TX!u$TX!w$TX!y$TX$f$TX~P!ETO#R'WO~P+sO]'XO~Ov'YO[#Wy~P+sOury$Rry~P;pO!S'ZO!T'ZO~O!R'[Omzivzi~Ox!kq!l!kq!n!kq!q!kq!s!kq!u!kq!w!kq!y!kq$f!kq~P;pO!R&pOvzaxza!lza!nza!qza!sza!uza!wza!yza$fza~Ov'}Ox$Ua!l$Ua!n$Ua!q$Ua!s$Ua!u$Ua!w$Ua!y$Ua$f$Ua~O!^'rO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOv!Uix!Ui!V!Ui!W!Ui!X!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui{!Ui|!Ui}!Ui!R!Ui~O$W!xO~P# wO$W!Ui~P# wO!^'rO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOv!Uix!Ui!V!Ui!W!Ui!X!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$W!Ui$Y!Ui$f!Ui{!Ui|!Ui}!Ui!R!Ui~O$X!Ui~P#$SO$X!yO~P#$SO$_!}O$`!}O$a!}O$b!}O$c#OOv!Uix!Ui!V!Ui!W!Ui!X!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$W!Ui$X!Ui$Y!Ui$Z!Ui$[!Ui$f!Ui{!Ui|!Ui}!Ui!R!Ui~O!^'rO$]!|O$^!|O~P#&_O!^!Ui$]!Ui$^!Ui~P#&_Ov!Ui{!Ui|!Ui}!Ui!R!Ui~P!9QO!X'jO!^'rO$W!xO$X!yO$Y!zO$Z!{O$[!{O$]!|O$^!|O$_!}O$`!}O$a!}O$b!}O$c#OOv!Uix!Ui!V!Ui!l!Ui!n!Ui!q!Ui!s!Ui!u!Ui!w!Ui!y!Ui$f!Ui{!Ui|!Ui}!Ui!R!Ui~O!W'zO~P#)TO!W!Ui~P#)TOx!rq!l!rq!n!rq!q!rq!s!rq!u!rq!w!rq!y!rq$f!rq~P;pOv'{Ox$Ta!l$Ta!n$Ta!q$Ta!s$Ta!u$Ta!w$Ta!y$Ta$f$Ta~O]'`O~O!S'cO!T'cO~O!R'dOmzqvzq~O!R'[Ovzixzi!lzi!nzi!qzi!szi!uzi!wzi!yzi$fzi~Ov#kax#ka!l#ka!n#ka!q#ka!s#ka!u#ka!w#ka!y#ka$f#ka~P!ETO[#W!Z~P+sO!S'fO!T'fO~O!R'dOvzqxzq!lzq!nzq!qzq!szq!uzq!wzq!yzq$fzq~O!O(OO#yPO~OP$^~", - goto: "F}$jPPP$kP%ZP%hP%ZP%ZPP%ZPPPPPP(c*RP*RPP+xPP-oP(cP/n/n/nPP/tPP/zPPPP0U0ePPP(jPPP0h1e2h3hP4n5w7T(jP8g8g8g(j(jP:S:WP:WP:W:WP:WP:WP:WP:WP(jP%ZPP%ZPPPP%ZP:[:[:_P:b%Z:n%ZP%Z:q%Z;O;R;X;[P;k;zRQQO7+$nOOQO-E8u-E8uOOQO7+'S7+'SO!>YQPO'#DWO!>pQPO'#FdOOQO1G/Z1G/ZO!vQQO7+%yO!>xQQO'#EyO!?TQPO,5OAN>OOGnQQO'#DnO!.wQQO'#DnOGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|OGnQQO,59|O!.wQQO,59|O!.wQQO,59kO!.wQQO7+$pO#0bQPO,59rO!.wQQO'#EzO/|QPO1G/^O#0jQPO1G/UO#0oQPO,59jO!vQQO'#DO", + stateData: "#0y~O#xOS#yOSPOS~OSZOUQOWZOY`O[aO_bOlTO!OfO#OcO#RdO#WeO#agO#ihO#{PO~O#vRP~P]OgkOilOlnOoqOqmO#}jO~O`yOayObyOcyOdyOlTOqmO!OzO!cxO!i{O#{PO#}jO$[vO$`uO$fwO~Og$OXi$OXl$OXo$OXq$OX#}$OX~Ov|O$S$jX~P#wOS#zXU#zXW#zXY#zX[#zX_#zXl#zX!O#zX#O#zX#R#zX#W#zX#a#zX#i#zX#v#zX#{#zX]#zX#P#zX#T#zX#U#zX~P#wO$S!OO~O#vRX]RX#PRX#TRX#URX~P]O]RP~P]O#PRP~P]O!O!aO#{PO~OS!eO#v#hX]#hX#P#hX#T#hX#U#hX~P!vOU!gO~O`yOayObyOcyOdyOi!lOlTOqmO!OzO!c'jO!i{O#{PO#}jO$[vO$`uO$fwO~Ou!oO~P'cOm!rOx!uO~P!vOm#|X!V#|X!W#|X!X#|X!^#|X$Y#|X$Z#|X$[#|X$]#|X$^#|X$_#|X$`#|X$a#|X$b#|X$c#|X$d#|X$e#|X[#|X#S#|XS#|Xv#|X#v#|Xj#|Xx#|XU#|XW#|XY#|X_#|X!O#|X#O#|X#R#|X#W#|X#a#|X#i#|X#{#|X]#|X#P#|X{#|X|#|X}#|X!R#|X#T#|X#U#|X~P!bO!V#WO!W#XO!X#PO!^#TO$Y!xO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OO~Om!wO~P+sO!|#YOm$OX!V$OX!W$OX!X$OX!^$OX$Y$OX$Z$OX$[$OX$]$OX$^$OX$_$OX$`$OX$a$OX$b$OX$c$OX$d$OX$e$OX[$OX#S$OXS$OXv$OX#v$OXj$OXx$OXU$OXW$OXY$OX_$OX!O$OX#O$OX#R$OX#W$OX#a$OX#i$OX#{$OX]$OX#P$OX{$OX|$OX}$OX!R$OX#T$OX#U$OX~P#wOl#[O~O$g#^O~OlTO#{PO~Ov|O$S$ja~O]#bO~O[#cO~P+sO#P#dO~O#S#eO~P+sOv#gO$S#fO#[$iX~O#[#iO~O[#jO~Og#kOo#mOl#_X~O$l#oOS#gPU#gPW#gPY#gP[#gP_#gPl#gPv#gP!O#gP#O#gP#R#gP#W#gP#a#gP#i#gP#v#gP#{#gP$S#gP]#gP#P#gP#T#gP#U#gP~Ov#qOS$kXU$kXW$kXY$kX[$kX_$kXl$kX!O$kX#O$kX#R$kX#W$kX#a$kX#i$kX#v$kX#{$kX$S$kX]$kX#P$kX#T$kX#U$kX~O$S#tOS#daU#daW#daY#da[#da_#dal#da!O#da#O#da#R#da#W#da#a#da#i#da#v#da#{#da]#da#P#da#T#da#U#da~Ov#uOS$VX#v$VXm$VXx$VXU$VXW$VXY$VX[$VX_$VXl$VX!O$VX#O$VX#R$VX#W$VX#a$VX#i$VX#{$VX]$VX#P$VX#T$VX#U$VX~P+sOS#wO#v#ha]#ha#P#ha#T#ha#U#ha~Oj#xO~P+sOu#|X!V#|X!W#|X!X#|X!^#|X$T#|X$Y#|X$Z#|X$[#|X$]#|X$^#|X$_#|X$`#|X$a#|X$b#|X$c#|X$d#|X$e#|Xx#|X!l#|X!o#|X!r#|X!t#|X!v#|X!x#|X!z#|X$h#|X~P!bOg$PXi$PXl$PXo$PXq$PX!V$PX!W$PX!X$PX!^$PX#}$PX$Y$PX$Z$PX$[$PX$]$PX$^$PX$_$PX$`$PX$a$PX$b$PX$c$PX$d$PX$e$PX~O$S#yOu$PX$T$PX~P9{O!V'zO!W'|O!X'lO!^'tO$Y!xO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OO~OutX$TtX~P;pOu#}O$T#{O~O!|#YOu$OX!V$OX!W$OX!X$OX!^$OX$T$OX$Y$OX$Z$OX$[$OX$]$OX$^$OX$_$OX$`$OX$a$OX$b$OX$c$OX$d$OX$e$OXx$OX!l$OX!o$OX!r$OX!t$OX!v$OX!x$OX!z$OX$h$OX~P#wOm$PO~Om$POx!uO~Oy$RO~OlnOqmO#}jO~Ol$^O~O$e#OO!V!ba!W!ba!X!ba!^!ba$Y!ba$Z!ba$[!ba$]!ba$^!ba$_!ba$`!ba$a!ba$b!ba$c!ba$d!bav!bax!ba{!ba|!ba}!ba!R!ba~Om!ba[!ba#S!baS!ba#v!baj!baU!baW!baY!ba_!bal!ba!O!ba#O!ba#R!ba#W!ba#a!ba#i!ba#{!ba]!ba#P!ba#T!ba#U!ba~P?vOc$`OlTO#{POm!QP~Ox$hO!l$fO!o$gO!r$iO!t$jO!v$kO!x$lO!z$mO~O$h$nO~PCUOv#ta$S#ta~P#wO]RP#TRP#URP~P]Ov#gO#[$ia~Og#kOo$yOl#_a~Ov#qOS$kaU$kaW$kaY$ka[$ka_$kal$ka!O$ka#O$ka#R$ka#W$ka#a$ka#i$ka#v$ka#{$ka$S$ka]$ka#P$ka#T$ka#U$ka~Ov#uOS$Va#v$Vam$Vax$VaU$VaW$VaY$Va[$Va_$Val$Va!O$Va#O$Va#R$Va#W$Va#a$Va#i$Va#{$Va]$Va#P$Va#T$Va#U$Va~O`yOayObyOcyOdyOlTOqmO!OzO!c'jO!i{O#{PO#}jO$[vO$`uO$fwO~Oj%TO~P+sOu%`O~P'cOu%`O$T%aO~O$e#OO!V!ba!W!ba!X!ba!^!ba$Y!ba$Z!ba$[!ba$]!ba$^!ba$_!ba$`!ba$a!ba$b!ba$c!ba$d!bax!ba!l!ba!o!ba!r!ba!t!ba!v!ba!x!ba!z!ba$h!ba~Ou!ba$T!ba~PIVOm%cO~O!^#TO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOm!Ui!V!Ui!W!Ui!X!Ui[!Ui#S!UiS!Uiv!Ui#v!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui#O!Ui#R!Ui#W!Ui#a!Ui#i!Ui#{!Ui]!Ui#P!Ui{!Ui|!Ui}!Ui!R!Ui#T!Ui#U!Ui~O$Y!xO~PKVO$Y!Ui~PKVO!^#TO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOm!Ui!V!Ui!W!Ui!X!Ui$Y!Ui$[!Ui[!Ui#S!UiS!Uiv!Ui#v!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui#O!Ui#R!Ui#W!Ui#a!Ui#i!Ui#{!Ui]!Ui#P!Ui{!Ui|!Ui}!Ui!R!Ui#T!Ui#U!Ui~O$Z!Ui~PN^O$Z!yO~PN^O$a!}O$b!}O$c!}O$d!}O$e#OOm!Ui!V!Ui!W!Ui!X!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Ui[!Ui#S!UiS!Uiv!Ui#v!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui#O!Ui#R!Ui#W!Ui#a!Ui#i!Ui#{!Ui]!Ui#P!Ui{!Ui|!Ui}!Ui!R!Ui#T!Ui#U!Ui~O!^#TO$_!|O$`!|O~P!#eO!^!Ui$_!Ui$`!Ui~P!#eO$e#OOm!Ui!V!Ui!W!Ui[!Ui#S!UiS!Uiv!Ui#v!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui#O!Ui#R!Ui#W!Ui#a!Ui#i!Ui#{!Ui]!Ui#P!Ui{!Ui|!Ui}!Ui!R!Ui#T!Ui#U!Ui~O!X!Ui!^!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Ui$_!Ui$`!Ui$a!Ui$b!Ui$c!Ui$d!Ui~P!&rO!X#PO!^#TO$Y!xO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOm!Ui!V!Ui[!Ui#S!UiS!Uiv!Ui#v!Uij!Uix!UiU!UiW!UiY!Ui_!Uil!Ui!O!Ui#O!Ui#R!Ui#W!Ui#a!Ui#i!Ui#{!Ui]!Ui#P!Ui{!Ui|!Ui}!Ui!R!Ui#T!Ui#U!Ui~O!W#XO~P!)rO!W!Ui~P!)rO!o%gO~Ov%hOm$XX~P#wOv%hOm$XX~Om%jO~O$h%lO~PCUO`yOayObyOcyOdyOi(VOlTOqmO!OzO!c'kO!i{O#{PO#}jO$[vO$`uO$fwO~Oy%tO~O`yOayObyOcyOdyOlTOqmO!OzO!c'kO!i{O#{PO#}jO$[vO$`uO$fwO~Oy%xO~O]%zO~OS!}qU!}qW!}qY!}q[!}q_!}ql!}q!O!}q#O!}q#R!}q#W!}q#a!}q#i!}q#v!}q#{!}q]!}q#P!}q#T!}q#U!}q~P+sO]%}O#T%|O#U&OO~Ov&PO~P+sO]&QO~O$m&SO~OS#lav#la#v#lam#lax#laU#laW#laY#la[#la_#lal#la!O#la#O#la#R#la#W#la#a#la#i#la#{#la]#la#P#la#T#la#U#la~P+sOusi$Tsi~P;pO$S&TO~O!^'tO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOu!Ui!V!Ui!W!Ui!X!Ui$T!Uix!Ui!l!Ui!o!Ui!r!Ui!t!Ui!v!Ui!x!Ui!z!Ui$h!Ui~O$Y!xO~P!3xO$Y!Ui~P!3xO!^'tO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOu!Ui!V!Ui!W!Ui!X!Ui$T!Ui$Y!Ui$[!Uix!Ui!l!Ui!o!Ui!r!Ui!t!Ui!v!Ui!x!Ui!z!Ui$h!Ui~O$Z!Ui~P!5zO$Z!yO~P!5zO$a!}O$b!}O$c!}O$d!}O$e#OOu!Ui!V!Ui!W!Ui!X!Ui$T!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Uix!Ui!l!Ui!o!Ui!r!Ui!t!Ui!v!Ui!x!Ui!z!Ui$h!Ui~O!^'tO$_!|O$`!|O~P!7|O!^!Ui$_!Ui$`!Ui~P!7|O$e#OO!V!Ui!W!Ui!X!Ui!^!Ui$Y!Ui$Z!Ui$[!Ui$]!Ui$^!Ui$_!Ui$`!Ui$a!Ui$b!Ui$c!Ui$d!Uix!Ui!l!Ui!o!Ui!r!Ui!t!Ui!v!Ui!x!Ui!z!Ui$h!Ui~Ou!Ui$T!Ui~P!:UO!X'lO!^'tO$Y!xO$Z!yO$[!zO$]!{O$^!{O$_!|O$`!|O$a!}O$b!}O$c!}O$d!}O$e#OOu!Ui!V!Ui$T!Uix!Ui!l!Ui!o!Ui!r!Ui!t!Ui!v!Ui!x!Ui!z!Ui$h!Ui~O!W'|O~P!WPBVDUFVF`PPFpFwGQGWPPPPPPPPPPPPPPPGZG^GkQ_OQ!SaQ!UcQ$o#cQ$q#eQ$v#jQ&a%jQ&n&OQ'Y&lR'c'ZgZO]ac#c#e#j%j&O&l'Z$[SOT]abcdhlnx|!O!l#P#Q#R#S#T#U#V#W#X#[#c#d#e#f#i#j#t#u#y$R$g$j$k$m%g%h%j%t%|&O&P&T&Z&h&l'Z']'j'k'l'm'n'o'p'q'r's't'u'v'w'x'y'z'{'|'}(O(P(R(VQiQQ!R`Q!WeQ![fS!^g#qQ!hkW!km#{%a&VQ!vqQ#s!aQ$s#gQ$w#kQ$z#mQ${#oW%n$f$i%x&cQ&R$yQ&s&YR'a(QYoRr!j!v%m$PyTbdhlmnx!O!l#P#Q#R#S#T#U#V#W#X#d#f#i#t#u#y#{$R$f$g$i$j$k$m%a%g%t%x%|&P&T&V&Z&c&h']'j'k'l'm'n'o'p'q'r's't'u'v'w'x'y'z'{'|'}(O(P(R(V$mSOT]abcdhlmnx|!O!l#P#Q#R#S#T#U#V#W#X#[#c#d#e#f#i#j#t#u#y#{$R$f$g$i$j$k$m%a%g%h%j%t%x%|&O&P&T&V&Z&c&h&l'Z']'j'k'l'm'n'o'p'q'r's't'u'v'w'x'y'z'{'|'}(O(P(R(V$mVOT]abcdhlmnx|!O!l#P#Q#R#S#T#U#V#W#X#[#c#d#e#f#i#j#t#u#y#{$R$f$g$i$j$k$m%a%g%h%j%t%x%|&O&P&T&V&Z&c&h&l'Z']'j'k'l'm'n'o'p'q'r's't'u'v'w'x'y'z'{'|'}(O(P(R(VUV|#[%hfWO]ac#c#e#j%j&O&l'Z!^tTbdhlnx!O!l#P#Q#R#S#T#U#V#W#X#d#f#i#t#u$R%g%|&P&Z'](Vx!qm#y#{$g$k$m%a&T&V&h'j'l'n'p'r't'v'x'z'|w%p$f$i$j%t%x&c'k'm'o'q's'u'w'y'{'}(O(P(Ra!nm#{$f$i%a%x&V&cQ!snR$Q!tQ%e$RQ&f%tT&u&Z(RQ#]zQ#n!]Q%O#sQ'`&tR'h(SR$b#[t#Qs!T!V!c!i#z$T$[$]$p$r%Q%d&]&m&o'd(Ud'n!m%S%U%]%^%s%w%y&p'Xa'o%o%v&e&y&z'R'S'gx#Rs!T!V!c!i#z$T$U$W$[$]$p$r%Q%d&]&m&o'd(Uh'p!m%S%U%V%X%]%^%s%w%y&p'Xe'q%o%v&e&y&z&{&}'R'S'gv#Ss!T!V!c!i#z$T$U$[$]$p$r%Q%d&]&m&o'd(Uf'r!m%S%U%V%]%^%s%w%y&p'Xc's%o%v&e&y&z&{'R'S'gz#Ts!T!V!c!i#z$T$U$V$W$[$]$p$r%Q%d&]&m&o'd(Uj't!m%S%U%V%W%X%]%^%s%w%y&p'Xg'u%o%v&e&y&z&{&|&}'R'S'g|#Us!T!V!c!i#z$T$U$V$W$X$[$]$p$r%Q%d&]&m&o'd(Ul'v!m%S%U%V%W%X%Y%]%^%s%w%y&p'Xi'w%o%v&e&y&z&{&|&}'O'R'S'g!O#Vs!T!V!c!i#z$T$U$V$W$X$Y$[$]$p$r%Q%d&]&m&o'd(Un'x!m%S%U%V%W%X%Y%Z%]%^%s%w%y&p'Xk'y%o%v&e&y&z&{&|&}'O'P'R'S'g!S#Vs!T!V!c!i#Z#z$T$U$V$W$X$Y$Z$[$]$p$r%Q%d&]&m&o'd(Ur'x!m$O%S%U%V%W%X%Y%Z%[%]%^%s%w%y&p'Xo'y%o%v&b&e&y&z&{&|&}'O'P'Q'R'S'g!^xTbdhlnx!O!l#P#Q#R#S#T#U#V#W#X#d#f#i#t#u$R%g%|&P&Z'](Vx'jm#y#{$g$k$m%a&T&V&h'j'l'n'p'r't'v'x'z'|w'k$f$i$j%t%x&c'k'm'o'q's'u'w'y'{'}(O(P(RT$d#^$eT$c#^$eQ%r$fQ%u$iR&i%xR!ZeR!YeQ!fhQ#a!OQ$u#iR%P#tR!]fgYO]ac#c#e#j%j&O&l'ZR!bgQ!_gR$|#qR#p!^d^Oac#c#e#j%j&O&l'ZR!Q]d]Oac#c#e#j%j&O&l'ZR!P]Q#|!pR%b#|Q#v!cR%R#vS%i$_$`R&`%iQ&[%eS&v&['WR'W&fQ$e#^R%k$eQ&d%qR'U&dQ%{$qR&j%{Q#h!WR$t#hQ#l![R$x#lQ}UR#`}Q#r!_R$}#rg[O]ac#c#e#j%j&O&l'ZQsTQ!TbQ!VdY!chn!O#i#tQ!ilW!mm#{%a&VQ#ZxQ#z!lQ$O'jQ$T#PQ$U#QQ$V#RQ$W#SQ$X#TQ$Y#UQ$Z#VQ$[#WQ$]#XQ$p#dQ$r#fQ%Q#uQ%S#yQ%U'lQ%V'nQ%W'pQ%X'rQ%Y'tQ%Z'vQ%['xQ%]'zQ%^'|S%d$R&ZW%o$f$i%x&cQ%s$gQ%v$jQ%w$kQ%y$mQ&]%gQ&b'kS&e%t(RQ&m%|Q&o&PQ&p&TQ&y(OQ&z'mQ&{'oQ&|'qQ&}'sQ'O'uQ'P'wQ'Q'yQ'R'{Q'S'}Q'X&hQ'd']Q'g(PR(U(VlRO]ac|#[#c#e#j%h%j&O&l'Z!^rTbdhlnx!O!l#P#Q#R#S#T#U#V#W#X#d#f#i#t#u$R%g%|&P&Z'](Vx!jm#y#{$g$k$m%a&T&V&h'j'l'n'p'r't'v'x'z'|w%m$f$i$j%t%x&c'k'm'o'q's'u'w'y'{'}(O(P(RfUO]ac#c#e#j%j&O&l'Z$OVTbdhlmnx!O!l#P#Q#R#S#T#U#V#W#X#d#f#i#t#u#y#{$R$f$g$i$j$k$m%a%g%t%x%|&P&T&V&Z&c&h']'j'k'l'm'n'o'p'q'r's't'u'v'w'x'y'z'{'|'}(O(P(R(VQ#_|Q$_#[R&^%hWpRr!j%mR$S!vQ!pmU%_#{%a&VU%q$f$i%xR'T&cZoRr!j!v%mW!dh!O#i#tR!tnQ%f$RR&g%tR$a#[R!XegXO]ac#c#e#j%j&O&l'ZR!`g", + nodeNames: "⚠ Comment Chunk Block ; Label :: Name break Goto goto Scope do end WhileStatement while nil true false Ellipsis Number LiteralString Property . MemberExpression [ ] Parens ( ) FunctionCall : TableConstructor { FieldDynamic FieldProp FieldExp } , AggOrderBy order by OrderBy desc asc using function FuncBody ArgList nulls first last BinaryExpression or and CompareOp BitOp BitOp BitOp BitOp Concat ArithOp ArithOp ArithOp UnaryExpression not ArithOp BitOp LenOp FunctionDef Query query QueryClause FromClause from FieldList WhereClause where OrderByClause SelectClause select LimitClause limit OffsetClause offset GroupByClause group HavingClause having FilteredCall filter RepeatStatement repeat until IfStatement if then elseif else ForStatement for ForNumeric ForGeneric NameList in ExpList Function FuncName LocalFunction local Assign VarList Local AttNameList AttName Attrib ReturnStatement return", + maxTerm: 167, nodeProps: [ - [ - "group", - -14, - 4, - 5, - 8, - 9, - 11, - 14, - 30, - 90, - 93, - 98, - 105, - 107, - 109, - 111, - "Statement", - -3, - 34, - 35, - 36, - "Field", - ], + ["group", -14,4,5,8,9,11,14,30,91,94,99,106,108,110,112,"Statement",-3,34,35,36,"Field"] ], - skippedNodes: [0, 1], - repeatNodeCount: 11, - tokenData: - "Dj~RuXY#fYZ$Q[]#f]^$_pq#fqr$grs$rst)`uv)evw)jwx)oxy.Wyz.]z{.b{|.g|}.l}!O.s!O!P/g!P!Q/|!Q!R0^!R![1u![!]3m!]!^3z!^!_4R!_!`4e!`!a4m!c!}5P!}#O5b#O#P#w#P#QCo#Q#RC|#R#S5P#T#o5P#o#pDR#p#qDW#q#rD]#r#sDb~#kS#w~XY#f[]#fpq#f#O#P#w~#zQYZ#f]^#f~$VP#v~]^$Y~$_O#v~~$dP#v~YZ$Ym$jP!_!`$mm$rO!Xmk$uXOY$rZ]$r^r$rrs%bs#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk%gO#{kk%j]rs$rwx$r!Q![&c#O#P$r#T#U$r#U#V$r#Y#Z$r#b#c$r#f#g$r#h#i$r#i#j(T#l#m(v#n#o$rk&fZOY$rZ]$r^r$rrs%bs!Q$r!Q!['X![#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk'[ZOY$rZ]$r^r$rrs%bs!Q$r!Q![$r![#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk(QP;=`<%l$rk(WP#o#p(Zk(^R!Q![(g!c!i(g#T#Z(gk(jS!Q![(g!c!i(g#T#Z(g#q#r$rk(yR!Q![)S!c!i)S#T#Z)Sk)VR!Q![$r!c!i$r#T#Z$r~)eO$d~~)jO$a~~)oO$X~k)rXOY)oZ])o^w)owx%bx#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok*b]rs)owx)o!Q![+Z#O#P)o#T#U)o#U#V)o#Y#Z)o#b#c)o#f#g)o#h#i)o#i#j,{#l#m-n#n#o)ok+^ZOY)oZ])o^w)owx%bx!Q)o!Q![,P![#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok,SZOY)oZ])o^w)owx%bx!Q)o!Q![)o![#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok,xP;=`<%l)ok-OP#o#p-Rk-UR!Q![-_!c!i-_#T#Z-_k-bS!Q![-_!c!i-_#T#Z-_#q#r)ok-qR!Q![-z!c!i-z#T#Z-zk-}R!Q![)o!c!i)o#T#Z)o~.]Ol~~.bOm~~.gO$_~~.lO$]~o.sOvc$R[~.xP$^~}!O.{~/QTP~OY.{Z].{^;'S.{;'S;=`/a<%lO.{~/dP;=`<%l.{o/lPgi!O!P/oo/tP!^m!O!P/wQ/|OcQ~0RQ$`~!P!Q0X!_!`$m~0^O$b~~0cUd~!O!P0u!Q![1u!g!h1Z!z!{2W#X#Y1Z#l#m2W~0xP!Q![0{~1QRd~!Q![0{!g!h1Z#X#Y1Z~1^R{|1g}!O1g!Q![1m~1jP!Q![1m~1rPd~!Q![1m~1zSd~!O!P0u!Q![1u!g!h1Z#X#Y1Z~2ZR!Q![2d!c!i2d#T#Z2d~2iUd~!O!P2{!Q![2d!c!i2d!r!s1Z#T#Z2d#d#e1Z~3OR!Q![3X!c!i3X#T#Z3X~3^Td~!Q![3X!c!i3X!r!s1Z#T#Z3X#d#e1Z~3rPo~![!]3u~3zOU~_4ROSR$R[o4YQ$jQ!Xm!^!_4`!_!`$mm4eO$Zm~4jP$Q~!_!`$mo4tQ$kQ!Xm!_!`$m!`!a4zm5PO$[m~5US#y~!Q![5P!c!}5P#R#S5P#T#o5Po5gQik!_!`5m!}#OB_k5pQ!_!`5v!}#O=ok5yP!}#O5|k6PTO#P5|#P#Q6`#Q;'S5|;'S;=`8W<%lO5|k6cVO!_6x!_!`:s!`#P6x#P#Q;w#Q;'S6x;'S;=`=i<%lO6xk6{VO!_7b!_!`8^!`#P7b#P#Q8v#Q;'S7b;'S;=`=c<%lO7bk7eTO#P5|#P#Q7t#Q;'S5|;'S;=`8W<%lO5|k7wP!_!`7zk7}P!_!`8Qk8TP#P#Q%bk8ZP;=`<%l5|k8aTO#P8^#P#Q7t#Q;'S8^;'S;=`8p<%lO8^k8sP;=`<%l8^k8yUO!_5|!_!`9]!`#P5|#Q;'S5|;'S;=`8W<%lO5|k9`VO!_5|!_!`9u!`#P5|#P#Q6`#Q;'S5|;'S;=`8W<%lO5|k9xTO#P5|#P#Q:X#Q;'S5|;'S;=`8W<%lO5|k:^V#{kO!_6x!_!`:s!`#P6x#P#Q;w#Q;'S6x;'S;=`=i<%lO6xk:vVO!_8^!_!`;]!`#P8^#P#Q7t#Q;'S8^;'S;=`8p<%lO8^k;`TO#P8^#P#Q;o#Q;'S8^;'S;=`8p<%lO8^k;tP#{k!_!`7zk;zVO!_7b!_!`:s!`#P7b#P#QR#Q;'S=o;'S;=`>}<%lO=ok>UVO!_>k!_!`?T!`#P>k#P#Q@X#Q;'S>k;'S;=`Ao<%lO>kk>nTO#P=o#P#Q7z#Q;'S=o;'S;=`>}<%lO=ok?QP;=`<%l=ok?WTO#P?g#P#Q@P#Q;'S?g;'S;=`?y<%lO?gk?jTO#P?g#P#Q7z#Q;'S?g;'S;=`?y<%lO?gk?|P;=`<%l?gk@UP#{k!_!`8Qk@[VO!_=o!_!`@q!`#P=o#P#QAu#Q;'S=o;'S;=`>}<%lO=ok@tTO#P=o#P#QAT#Q;'S=o;'S;=`>}<%lO=okAYV#{kO!_>k!_!`?T!`#P>k#P#Q@X#Q;'S>k;'S;=`Ao<%lO>kkArP;=`<%l>kkAxVO!_?g!_!`?T!`#P?g#P#QAu#Q;'S?g;'S;=`?y<%lO?goBdT$eSO#PBs#P#QCV#Q;'SBs;'S;=`Ci<%lOBskBvTO#PBs#P#QCV#Q;'SBs;'S;=`Ci<%lOBskCYTO#PBs#P#Q%b#Q;'SBs;'S;=`Ci<%lOBskClP;=`<%lBsoCtPjP#P#QCwnC|O$fn~DRO$c~~DWOq~~D]O$W~~DbOu~~DgP$Y~!_!`$m", + skippedNodes: [0,1], + repeatNodeCount: 12, + tokenData: "Dj~RuXY#fYZ$Q[]#f]^$_pq#fqr$grs$rst)`uv)evw)jwx)oxy.Wyz.]z{.b{|.g|}.l}!O.s!O!P/g!P!Q/|!Q!R0^!R![1u![!]3m!]!^3z!^!_4R!_!`4e!`!a4m!c!}5P!}#O5b#O#P#w#P#QCo#Q#RC|#R#S5P#T#o5P#o#pDR#p#qDW#q#rD]#r#sDb~#kS#y~XY#f[]#fpq#f#O#P#w~#zQYZ#f]^#f~$VP#x~]^$Y~$_O#x~~$dP#x~YZ$Ym$jP!_!`$mm$rO!Xmk$uXOY$rZ]$r^r$rrs%bs#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk%gO#}kk%j]rs$rwx$r!Q![&c#O#P$r#T#U$r#U#V$r#Y#Z$r#b#c$r#f#g$r#h#i$r#i#j(T#l#m(v#n#o$rk&fZOY$rZ]$r^r$rrs%bs!Q$r!Q!['X![#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk'[ZOY$rZ]$r^r$rrs%bs!Q$r!Q![$r![#O$r#O#P%g#P;'S$r;'S;=`'}<%lO$rk(QP;=`<%l$rk(WP#o#p(Zk(^R!Q![(g!c!i(g#T#Z(gk(jS!Q![(g!c!i(g#T#Z(g#q#r$rk(yR!Q![)S!c!i)S#T#Z)Sk)VR!Q![$r!c!i$r#T#Z$r~)eO$f~~)jO$c~~)oO$Z~k)rXOY)oZ])o^w)owx%bx#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok*b]rs)owx)o!Q![+Z#O#P)o#T#U)o#U#V)o#Y#Z)o#b#c)o#f#g)o#h#i)o#i#j,{#l#m-n#n#o)ok+^ZOY)oZ])o^w)owx%bx!Q)o!Q![,P![#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok,SZOY)oZ])o^w)owx%bx!Q)o!Q![)o![#O)o#O#P*_#P;'S)o;'S;=`,u<%lO)ok,xP;=`<%l)ok-OP#o#p-Rk-UR!Q![-_!c!i-_#T#Z-_k-bS!Q![-_!c!i-_#T#Z-_#q#r)ok-qR!Q![-z!c!i-z#T#Z-zk-}R!Q![)o!c!i)o#T#Z)o~.]Ol~~.bOm~~.gO$a~~.lO$_~o.sOvc$T[~.xP$`~}!O.{~/QTP~OY.{Z].{^;'S.{;'S;=`/a<%lO.{~/dP;=`<%l.{o/lPgi!O!P/oo/tP!^m!O!P/wQ/|OcQ~0RQ$b~!P!Q0X!_!`$m~0^O$d~~0cUd~!O!P0u!Q![1u!g!h1Z!z!{2W#X#Y1Z#l#m2W~0xP!Q![0{~1QRd~!Q![0{!g!h1Z#X#Y1Z~1^R{|1g}!O1g!Q![1m~1jP!Q![1m~1rPd~!Q![1m~1zSd~!O!P0u!Q![1u!g!h1Z#X#Y1Z~2ZR!Q![2d!c!i2d#T#Z2d~2iUd~!O!P2{!Q![2d!c!i2d!r!s1Z#T#Z2d#d#e1Z~3OR!Q![3X!c!i3X#T#Z3X~3^Td~!Q![3X!c!i3X!r!s1Z#T#Z3X#d#e1Z~3rPo~![!]3u~3zOU~_4ROSR$T[o4YQ$lQ!Xm!^!_4`!_!`$mm4eO$]m~4jP$S~!_!`$mo4tQ$mQ!Xm!_!`$m!`!a4zm5PO$^m~5US#{~!Q![5P!c!}5P#R#S5P#T#o5Po5gQik!_!`5m!}#OB_k5pQ!_!`5v!}#O=ok5yP!}#O5|k6PTO#P5|#P#Q6`#Q;'S5|;'S;=`8W<%lO5|k6cVO!_6x!_!`:s!`#P6x#P#Q;w#Q;'S6x;'S;=`=i<%lO6xk6{VO!_7b!_!`8^!`#P7b#P#Q8v#Q;'S7b;'S;=`=c<%lO7bk7eTO#P5|#P#Q7t#Q;'S5|;'S;=`8W<%lO5|k7wP!_!`7zk7}P!_!`8Qk8TP#P#Q%bk8ZP;=`<%l5|k8aTO#P8^#P#Q7t#Q;'S8^;'S;=`8p<%lO8^k8sP;=`<%l8^k8yUO!_5|!_!`9]!`#P5|#Q;'S5|;'S;=`8W<%lO5|k9`VO!_5|!_!`9u!`#P5|#P#Q6`#Q;'S5|;'S;=`8W<%lO5|k9xTO#P5|#P#Q:X#Q;'S5|;'S;=`8W<%lO5|k:^V#}kO!_6x!_!`:s!`#P6x#P#Q;w#Q;'S6x;'S;=`=i<%lO6xk:vVO!_8^!_!`;]!`#P8^#P#Q7t#Q;'S8^;'S;=`8p<%lO8^k;`TO#P8^#P#Q;o#Q;'S8^;'S;=`8p<%lO8^k;tP#}k!_!`7zk;zVO!_7b!_!`:s!`#P7b#P#QR#Q;'S=o;'S;=`>}<%lO=ok>UVO!_>k!_!`?T!`#P>k#P#Q@X#Q;'S>k;'S;=`Ao<%lO>kk>nTO#P=o#P#Q7z#Q;'S=o;'S;=`>}<%lO=ok?QP;=`<%l=ok?WTO#P?g#P#Q@P#Q;'S?g;'S;=`?y<%lO?gk?jTO#P?g#P#Q7z#Q;'S?g;'S;=`?y<%lO?gk?|P;=`<%l?gk@UP#}k!_!`8Qk@[VO!_=o!_!`@q!`#P=o#P#QAu#Q;'S=o;'S;=`>}<%lO=ok@tTO#P=o#P#QAT#Q;'S=o;'S;=`>}<%lO=okAYV#}kO!_>k!_!`?T!`#P>k#P#Q@X#Q;'S>k;'S;=`Ao<%lO>kkArP;=`<%l>kkAxVO!_?g!_!`?T!`#P?g#P#QAu#Q;'S?g;'S;=`?y<%lO?goBdT$gSO#PBs#P#QCV#Q;'SBs;'S;=`Ci<%lOBskBvTO#PBs#P#QCV#Q;'SBs;'S;=`Ci<%lOBskCYTO#PBs#P#Q%b#Q;'SBs;'S;=`Ci<%lOBskClP;=`<%lBsoCtPjP#P#QCwnC|O$hn~DRO$e~~DWOq~~D]O$Y~~DbOu~~DgP$[~!_!`$m", tokenizers: [0, 1, 2, 3, 4], - topRules: { Chunk: [0, 2] }, - dynamicPrecedences: { 142: 1 }, - specialized: [{ term: 133, get: (value) => spec_identifier[value] || -1 }], - tokenPrec: 4922, -}); + topRules: {"Chunk":[0,2]}, + dynamicPrecedences: {"144":1}, + specialized: [{term: 135, get: (value) => spec_identifier[value] || -1}], + tokenPrec: 4960 +}) diff --git a/client/space_lua/parse-lua.terms.js b/client/space_lua/parse-lua.terms.js index b6c66aa1..fc7146cd 100644 --- a/client/space_lua/parse-lua.terms.js +++ b/client/space_lua/parse-lua.terms.js @@ -1,5 +1,6 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -export const Comment = 1, +export const + Comment = 1, Chunk = 2, Block = 3, Label = 5, @@ -18,22 +19,23 @@ export const Comment = 1, Query = 70, QueryClause = 72, FromClause = 73, - WhereClause = 75, - OrderByClause = 77, - SelectClause = 78, - LimitClause = 80, - OffsetClause = 82, - GroupByClause = 84, - HavingClause = 86, - IfStatement = 93, - ForStatement = 98, - ForNumeric = 100, - ForGeneric = 101, - NameList = 102, - ExpList = 104, - FuncName = 106, - VarList = 110, - AttNameList = 112, - AttName = 113, - Attrib = 114, - ReturnStatement = 115; + FieldList = 75, + WhereClause = 76, + OrderByClause = 78, + SelectClause = 79, + LimitClause = 81, + OffsetClause = 83, + GroupByClause = 85, + HavingClause = 87, + IfStatement = 94, + ForStatement = 99, + ForNumeric = 101, + ForGeneric = 102, + NameList = 103, + ExpList = 105, + FuncName = 107, + VarList = 111, + AttNameList = 113, + AttName = 114, + Attrib = 115, + ReturnStatement = 116 diff --git a/client/space_lua/parse.ts b/client/space_lua/parse.ts index 55876ebb..2871f5be 100644 --- a/client/space_lua/parse.ts +++ b/client/space_lua/parse.ts @@ -127,10 +127,22 @@ function expressionHasFunctionDef(e: LuaExpression): boolean { const c = e.clauses[i]; switch (c.type) { case "From": - if (expressionHasFunctionDef(c.expression)) return true; + case "Select": + case "GroupBy": + for (const f of c.fields) { + switch (f.type) { + case "DynamicField": + if (expressionHasFunctionDef(f.key)) return true; + if (expressionHasFunctionDef(f.value)) return true; + break; + case "PropField": + case "ExpressionField": + if (expressionHasFunctionDef(f.value)) return true; + break; + } + } break; case "Where": - case "Select": case "Having": if (expressionHasFunctionDef(c.expression)) return true; break; @@ -151,11 +163,6 @@ function expressionHasFunctionDef(e: LuaExpression): boolean { } } break; - case "GroupBy": - for (let j = 0; j < c.expressions.length; j++) { - if (expressionHasFunctionDef(c.expressions[j])) return true; - } - break; } } return false; @@ -238,10 +245,22 @@ function exprReferencesNames(e: LuaExpression, names: Set): boolean { const c = e.clauses[i]; switch (c.type) { case "From": - if (exprReferencesNames(c.expression, names)) return true; + case "Select": + case "GroupBy": + for (const f of c.fields) { + switch (f.type) { + case "DynamicField": + if (exprReferencesNames(f.key, names)) return true; + if (exprReferencesNames(f.value, names)) return true; + break; + case "PropField": + case "ExpressionField": + if (exprReferencesNames(f.value, names)) return true; + break; + } + } break; case "Where": - case "Select": case "Having": if (exprReferencesNames(c.expression, names)) return true; break; @@ -262,11 +281,6 @@ function exprReferencesNames(e: LuaExpression, names: Set): boolean { } } break; - case "GroupBy": - for (let j = 0; j < c.expressions.length; j++) { - if (exprReferencesNames(c.expressions[j], names)) return true; - } - break; } } return false; @@ -564,10 +578,22 @@ function exprCapturesNames(e: LuaExpression, names: Set): boolean { const c = e.clauses[i]; switch (c.type) { case "From": - if (exprCapturesNames(c.expression, names)) return true; + case "Select": + case "GroupBy": + for (const f of c.fields) { + switch (f.type) { + case "DynamicField": + if (exprCapturesNames(f.key, names)) return true; + if (exprCapturesNames(f.value, names)) return true; + break; + case "PropField": + case "ExpressionField": + if (exprCapturesNames(f.value, names)) return true; + break; + } + } break; case "Where": - case "Select": case "Having": if (exprCapturesNames(c.expression, names)) return true; break; @@ -586,11 +612,6 @@ function exprCapturesNames(e: LuaExpression, names: Set): boolean { } } break; - case "GroupBy": - for (let j = 0; j < c.expressions.length; j++) { - if (exprCapturesNames(c.expressions[j], names)) return true; - } - break; } } return false; @@ -1307,6 +1328,20 @@ function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression { } } +function parseFieldList(t: ParseTree, ctx: ASTCtx): LuaTableField[] { + if (t.type !== "FieldList") { + throw new Error(`Expected FieldList, got ${t.type}`); + } + return t + .children!.filter( + (c) => + c.type === "FieldExp" || + c.type === "FieldProp" || + c.type === "FieldDynamic", + ) + .map((c) => parseTableField(c, ctx)); +} + function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause { if (t.type !== "QueryClause") { throw new Error(`Expected QueryClause, got ${t.type}`); @@ -1314,18 +1349,14 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause { t = t.children![0]; switch (t.type) { case "FromClause": { - if (t.children!.length === 4) { - // From clause with a name - return { - type: "From", - name: t.children![1].children![0].text!, - expression: parseExpression(t.children![3], ctx), - ctx: context(t, ctx), - }; + // children: ckw<"from">, FieldList + const fieldListNode = t.children!.find((c) => c.type === "FieldList"); + if (!fieldListNode) { + throw new Error("FromClause missing FieldList"); } return { type: "From", - expression: parseExpression(t.children![1], ctx), + fields: parseFieldList(fieldListNode, ctx), ctx: context(t, ctx), }; } @@ -1368,26 +1399,26 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause { }; } case "SelectClause": { + // children: ckw<"select">, FieldList + const fieldListNode = t.children!.find((c) => c.type === "FieldList"); + if (!fieldListNode) { + throw new Error("SelectClause missing FieldList"); + } return { type: "Select", - expression: parseExpression(t.children![1], ctx), + fields: parseFieldList(fieldListNode, ctx), ctx: context(t, ctx), }; } case "GroupByClause": { - // children: ckw<"group">, ckw<"by">, exp, ",", exp, ... - const expressions: LuaExpression[] = t - .children!.filter( - (c) => - c.type !== undefined && - c.type !== "group" && - c.type !== "by" && - c.type !== ",", - ) - .map((c) => parseExpression(c, ctx)); + // children: ckw<"group">, ckw<"by">, FieldList + const fieldListNode = t.children!.find((c) => c.type === "FieldList"); + if (!fieldListNode) { + throw new Error("GroupByClause missing FieldList"); + } return { type: "GroupBy", - expressions, + fields: parseFieldList(fieldListNode, ctx), ctx: context(t, ctx), }; } diff --git a/client/space_lua/query_collection.ts b/client/space_lua/query_collection.ts index 27a1649e..cbf9f62d 100644 --- a/client/space_lua/query_collection.ts +++ b/client/space_lua/query_collection.ts @@ -77,19 +77,32 @@ function buildGroupItemEnv( itemEnv.setLocal("group", groupVal); } + // Unpack named fields from multi-key LuaTable keys if (keyVal instanceof LuaTable) { for (const k of luaKeys(keyVal)) { if (typeof k !== "string") continue; itemEnv.setLocal(k, luaGet(keyVal, k, sf.astCtx ?? null, sf)); } } - // For single-key group by, bind the key name as a local - if ( - !(keyVal instanceof LuaTable) && - groupByNames && - groupByNames.length === 1 - ) { - itemEnv.setLocal(groupByNames[0], keyVal); + + // Bind all `group by` aliases/names to their key values. For + // single key bind the name to the scalar `keyVal`. For multi-key + // bind each name to the field from the key table. + if (groupByNames && groupByNames.length > 0) { + if (!(keyVal instanceof LuaTable)) { + // Bind all names to scalar + for (const gbn of groupByNames) { + itemEnv.setLocal(gbn, keyVal); + } + } else { + // Ensure every alias is bound even if `luaKeys` missed it + for (const gbn of groupByNames) { + const v = keyVal.rawGet(gbn); + if (v !== undefined) { + itemEnv.setLocal(gbn, v); + } + } + } } } return itemEnv; @@ -139,6 +152,11 @@ export type LuaOrderBy = { using?: string | LuaFunctionBody; }; +export type LuaGroupByEntry = { + expr: LuaExpression; + alias?: string; +}; + /** * Represents a query for a collection */ @@ -156,8 +174,8 @@ export type LuaCollectionQuery = { offset?: number; // Whether to return only distinct values distinct?: boolean; - // The group by expressions evaluated with Lua - groupBy?: LuaExpression[]; + // The group by entries evaluated with Lua + groupBy?: LuaGroupByEntry[]; // The having expression evaluated with Lua having?: LuaExpression; }; @@ -697,21 +715,20 @@ export async function applyQuery( let groupByNames: string[] | undefined; if (query.groupBy) { - // Extract bare names from group-by expressions for local binding - groupByNames = query.groupBy - .map((expr) => { - if (expr.type === "Variable") { - return expr.name; - } - if (expr.type === "PropertyAccess") { - return expr.property; - } + // Extract expressions and names from `group by` entries + const groupByEntries = query.groupBy; + + // Derive canonical name (explicit alias first, or from expression) + groupByNames = groupByEntries + .map((entry) => { + if (entry.alias) return entry.alias; + if (entry.expr.type === "Variable") return entry.expr.name; + if (entry.expr.type === "PropertyAccess") return entry.expr.property; return undefined as unknown as string; }) .filter(Boolean); const groups = new Map(); - const groupByExprs = query.groupBy as LuaExpression[]; for (const item of results) { const itemEnv = buildItemEnvLocal(query.objectVariable, item, env, sf); @@ -719,18 +736,19 @@ export async function applyQuery( const keyParts: any[] = []; const keyRecord: Record = {}; - for (const expr of groupByExprs) { - if (expr.type === "Variable") { - const v = await evalExpression(expr, itemEnv, sf); - keyParts.push(v); - keyRecord[expr.name] = v; - } else if (expr.type === "PropertyAccess") { - const v = await evalExpression(expr, itemEnv, sf); - keyParts.push(v); - keyRecord[expr.property] = v; - } else { - const v = await evalExpression(expr, itemEnv, sf); - keyParts.push(v); + for (let ei = 0; ei < groupByEntries.length; ei++) { + const entry = groupByEntries[ei]; + const v = await evalExpression(entry.expr, itemEnv, sf); + keyParts.push(v); + // Use alias if provided, or from expression + const name = + entry.alias ?? + (entry.expr.type === "Variable" ? entry.expr.name : undefined) ?? + (entry.expr.type === "PropertyAccess" + ? entry.expr.property + : undefined); + if (name) { + keyRecord[name] = v; } } @@ -829,13 +847,15 @@ export async function applyQuery( let selectResults: any[] | undefined; + // Pre-compute select for grouped + ordered queries if (grouped && query.select && query.orderBy) { + const selectExpr = query.select; selectResults = []; for (const item of results) { const itemEnv = mkEnv(query.objectVariable, item, env, sf); const groupTable = (item as LuaTable).rawGet("group"); const selected = await evalExpressionWithAggregates( - query.select, + selectExpr, itemEnv, sf, groupTable, @@ -918,6 +938,7 @@ export async function applyQuery( } if (query.select) { + const selectExpr = query.select; if (selectResults) { results = selectResults; } else { @@ -928,7 +949,7 @@ export async function applyQuery( const groupTable = (item as LuaTable).rawGet("group"); newResult.push( await evalExpressionWithAggregates( - query.select, + selectExpr, itemEnv, sf, groupTable,