From c299bcddae26d675b566c03ec7c9176169e595f9 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Thu, 3 Feb 2022 07:44:47 -0600 Subject: [PATCH] ecs: with, select queries --- src/codemirror/ecs.js | 4 +- src/common/ecs/compiler.ts | 20 ++++-- src/common/ecs/ecs.ts | 144 +++++++++++++++++++++---------------- src/test/testecs.ts | 10 +-- src/worker/tools/ecs.ts | 1 + 5 files changed, 103 insertions(+), 76 deletions(-) diff --git a/src/codemirror/ecs.js b/src/codemirror/ecs.js index a54a7e91..10b835a2 100644 --- a/src/codemirror/ecs.js +++ b/src/codemirror/ecs.js @@ -17,10 +17,10 @@ var keywords1, keywords2; var directives_list = [ - 'end', 'component', 'system', 'entity', 'scope', 'using', + 'end', 'component', 'system', 'entity', 'scope', 'using', 'demo', 'const', 'init', 'locals', 'on', 'do', 'emit', 'limit', - 'once', 'foreach', 'source', 'join' + 'once', 'foreach', 'with', 'join' ]; var keywords_list = [ 'processor', diff --git a/src/common/ecs/compiler.ts b/src/common/ecs/compiler.ts index 922f0f37..f187d428 100644 --- a/src/common/ecs/compiler.ts +++ b/src/common/ecs/compiler.ts @@ -1,7 +1,7 @@ import { mergeLocs, Tokenizer, TokenType } from "../tokenizer"; import { SourceLocated } from "../workertypes"; -import { Action, ArrayType, ComponentType, DataField, DataType, DataValue, Dialect_CA65, Entity, EntityArchetype, EntityManager, EntityScope, IntType, Query, RefType, SelectType, SourceFileExport, System } from "./ecs"; +import { Action, ArrayType, ComponentType, DataField, DataType, DataValue, Entity, EntityArchetype, EntityManager, EntityScope, IntType, Query, RefType, SelectType, SourceFileExport, System } from "./ecs"; export enum ECSTokenType { Ellipsis = 'ellipsis', @@ -196,7 +196,6 @@ export class ECSCompiler extends Tokenizer { parseResource(): System { let name = this.expectIdent().str; - let query = this.parseQuery(); let tempbytes; if (this.peekToken().str == 'locals') { this.consumeToken(); @@ -204,7 +203,7 @@ export class ECSCompiler extends Tokenizer { } let text = this.parseCode(); let select : SelectType = 'once'; - let action : Action = { text, event: name, query, select }; + let action : Action = { text, event: name, select }; return { name, tempbytes, actions: [action] }; } @@ -212,10 +211,17 @@ export class ECSCompiler extends Tokenizer { // TODO: unused events? let event = this.expectIdent().str; this.expectToken('do'); - let select = this.expectTokens(['once', 'foreach', 'source', 'join']).str as SelectType; // TODO: type check? - let query = this.parseQuery(); + let select = this.expectTokens( + ['once', 'foreach', 'join', 'with', 'select']).str as SelectType; // TODO: type check? + let query = undefined; let join = undefined; - if (select == 'join') join = this.parseQuery(); + if (select != 'once') { + query = this.parseQuery(); + } + if (select == 'join') { + this.expectToken('with'); + join = this.parseQuery(); + } let emits; let limit; if (this.peekToken().str == 'limit') { @@ -231,7 +237,7 @@ export class ECSCompiler extends Tokenizer { } let text = this.parseCode(); let action = { text, event, query, join, select, limit }; - return action; + return action as Action; } parseQuery() { diff --git a/src/common/ecs/ecs.ts b/src/common/ecs/ecs.ts index dc1e3af9..454173ad 100644 --- a/src/common/ecs/ecs.ts +++ b/src/common/ecs/ecs.ts @@ -111,17 +111,31 @@ export interface System extends SourceLocated { tempbytes?: number; } -export interface Action extends SourceLocated { - text: string; +export type SelectType = 'once' | 'foreach' | 'join' | 'with' | 'select'; + +export interface ActionBase extends SourceLocated { + select: SelectType; event: string; - select: SelectType - query: Query; - join?: Query; - limit?: number; + text: string; emits?: string[]; } -export type SelectType = 'once' | 'foreach' | 'source' | 'join'; +export interface ActionOnce extends ActionBase { + select: 'once' +} + +export interface ActionWithQuery extends ActionBase { + select: 'foreach' | 'join' | 'with' | 'select' + query: Query + limit?: number +} + +export interface ActionWithJoin extends ActionWithQuery { + select: 'join' + join?: Query +} + +export type Action = ActionWithQuery | ActionWithJoin | ActionOnce; export type DataValue = number | boolean | Uint8Array | Uint16Array; @@ -491,8 +505,10 @@ class ActionEval { readonly action: Action) { this.em = scope.em; this.dialect = scope.em.dialect; - this.qr = new QueryResult(scope, action.query); this.oldState = scope.state; + let q = (action as ActionWithQuery).query; + if (q) this.qr = new QueryResult(scope, q); + else this.qr = new QueryResult(scope, undefined, [], []); } begin() { let state = this.scope.state = Object.assign({}, this.scope.state); @@ -506,19 +522,21 @@ class ActionEval { case 'join': if (state.x || state.y) throw new ECSError('no free index registers for join', this.action); state.y = this.qr; - if (this.action.join) { - this.jr = new QueryResult(this.scope, this.action.join); - state.x = this.jr; - } + this.jr = new QueryResult(this.scope, (this.action as ActionWithJoin).join); + state.x = this.jr; break; - case 'source': - if (!state.x) throw new ECSError('expected index register', this.action); - let int = state.x.intersection(this.qr); - // TODO: what if we filter 0 entities? - if (int.entities.length == 0) throw new ECSError('queries do not intersect', this.action); - let indofs = int.entities[0].id - state.x.entities[0].id; - state.xofs += indofs; - state.x = int; + case 'with': + if (state.x) { + let int = state.x.intersection(this.qr); + // TODO: what if we filter 0 entities? + if (int.entities.length == 0) throw new ECSError('queries do not intersect', this.action); + let indofs = int.entities[0].id - state.x.entities[0].id; + state.xofs += indofs; + state.x = int; + } else { + if (this.qr.entities.length != 1) + throw new ECSError(`query outside of loop must match exactly one entity`, this.action); + } break; } } @@ -532,43 +550,46 @@ class ActionEval { let action = this.action; let sys = this.sys; let code = action.text; - let label = `${sys.name}__${action.event}`; // TODO: better label that won't conflict (seq?) - // TODO: detect cycles - // TODO: "source"? - // TODO: what if only 1 item? + let label = `${sys.name}__${action.event}__${this.em.seq++}`; // TODO: better label that won't conflict (seq?) let props: { [name: string]: string } = {}; - if (action.select == 'foreach') { - code = this.wrapCodeInLoop(code, action, this.qr.entities); - } - if (action.select == 'join' && this.jr) { - let jentities = this.jr.entities; - if (jentities.length == 0) - throw new ECSError(`join query for ${label} doesn't match any entities`, action.join); // TODO - let joinfield = this.getJoinField(action, this.qr.atypes, this.jr.atypes); + if (action.select != 'once') { + // TODO: detect cycles + // TODO: "source"? // TODO: what if only 1 item? - // TODO: should be able to access fields via Y reg - code = this.wrapCodeInLoop(code, action, this.qr.entities, joinfield); - props['%joinfield'] = this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0); //TODO? - this.qr = this.jr; // TODO? + if (action.select == 'foreach') { + code = this.wrapCodeInLoop(code, action, this.qr.entities); + } + if (action.select == 'join' && this.jr) { + let jentities = this.jr.entities; + if (jentities.length == 0) + throw new ECSError(`join query doesn't match any entities`, action); // TODO + let joinfield = this.getJoinField(action, this.qr.atypes, this.jr.atypes); + // TODO: what if only 1 item? + // TODO: should be able to access fields via Y reg + code = this.wrapCodeInLoop(code, action, this.qr.entities, joinfield); + props['%joinfield'] = this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0); //TODO? + this.qr = this.jr; // TODO? + } + let entities = this.qr.entities; + props['%efullcount'] = entities.length.toString(); + if (action.limit) { + entities = entities.slice(0, action.limit); + } + if (entities.length == 0) + throw new ECSError(`query doesn't match any entities`, action.query); // TODO + // filter entities from loop? + if (action.select == 'with' && entities.length > 1) { + // TODO: what if not needed + code = this.wrapCodeInFilter(code); + } + // define properties + props['%elo'] = entities[0].id.toString(); + props['%ehi'] = entities[entities.length - 1].id.toString(); + props['%ecount'] = entities.length.toString(); + props['%xofs'] = this.scope.state.xofs.toString(); + props['%yofs'] = this.scope.state.yofs.toString(); + this.qr.entities = entities; } - if (action.select == 'source') { - // TODO: what if not needed - code = this.wrapCodeInFilter(code); - } - let entities = this.qr.entities; - props['%efullcount'] = entities.length.toString(); - if (action.limit) { - entities = entities.slice(0, action.limit); - } - if (entities.length == 0) - throw new ECSError(`query for ${label} doesn't match any entities`, action.query); // TODO - // define properties - props['%elo'] = entities[0].id.toString(); - props['%ehi'] = entities[entities.length - 1].id.toString(); - props['%ecount'] = entities.length.toString(); - props['%xofs'] = this.scope.state.xofs.toString(); - props['%yofs'] = this.scope.state.yofs.toString(); - this.qr.entities = entities; // replace @labels code = code.replace(label_re, (s: string, a: string) => `${label}__${a}`); // replace {{...}} tags @@ -664,16 +685,14 @@ class ActionEval { // TODO: don't mix const and init data let range = this.scope.bss.getFieldRange(component, fieldName) || this.scope.rodata.getFieldRange(component, fieldName); if (!range) throw new ECSError(`couldn't find field for ${component.name}:${fieldName}, maybe no entities?`); // TODO - let eidofs = range.elo - qr.entities[0].id; // TODO // TODO: dialect let ident = this.dialect.fieldsymbol(component, field, bitofs); if (qualified) { return this.dialect.absolute(ident); - } else if (action.select == 'once') { - if (qr.entities.length != 1) - throw new ECSError(`can't choose multiple entities for ${fieldName} with select=once`, action); + } else if (qr.entities.length == 1) { return this.dialect.absolute(ident); } else { + let eidofs = range.elo - qr.entities[0].id; // TODO // TODO: eidofs? let ir; if (this.scope.state.x?.intersection(this.qr)) { @@ -697,8 +716,8 @@ class ActionEval { getJoinField(action: Action, atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]): ComponentFieldPair { let refs = Array.from(this.scope.iterateArchetypeFields(atypes, (c, f) => f.dtype == 'ref')); // TODO: better error message - if (refs.length == 0) throw new ECSError(`cannot find join fields`, action.query); - if (refs.length > 1) throw new ECSError(`cannot join multiple fields`, action.query); + if (refs.length == 0) throw new ECSError(`cannot find join fields`, action); + if (refs.length > 1) throw new ECSError(`cannot join multiple fields`, action); // TODO: check to make sure join works return refs[0]; // TODO /* TODO @@ -1043,6 +1062,8 @@ export class EntityManager { symbols: { [name: string]: 'init' | 'const' } = {}; event2systems: { [event: string]: System[] } = {}; name2cfpairs: { [cfname: string]: ComponentFieldPair[] } = {}; + mainPath: string = ''; + seq = 1; constructor(public readonly dialect: Dialect_CA65) { } @@ -1140,8 +1161,7 @@ export class EntityManager { exportToFile(file: SourceFileExport) { file.text(this.dialect.HEADER); // TODO for (let scope of Object.values(this.topScopes)) { - // TODO: demos - if (!scope.isDemo) { + if (!scope.isDemo || scope.filePath == this.mainPath) { scope.dump(file); } } diff --git a/src/test/testecs.ts b/src/test/testecs.ts index 1b626032..0af4fede 100644 --- a/src/test/testecs.ts +++ b/src/test/testecs.ts @@ -210,18 +210,18 @@ function testECS() { tempbytes: 12, actions: [ { - text: TEMPLATE4_S1, event: 'preframe', select: 'once', query: { + text: TEMPLATE4_S1, event: 'preframe', select: 'with', query: { include: [c_kernel] } }, { // TODO: should include kernel for numlines - text: TEMPLATE4_S2, event: 'preframe', select: 'once', query: { + text: TEMPLATE4_S2, event: 'preframe', select: 'with', query: { include: [c_sprite, c_hasbitmap, c_hascolormap, c_ypos], }, }, { - text: TEMPLATE4_K, event: 'kernel', select: 'once', query: { + text: TEMPLATE4_K, event: 'kernel', select: 'with', query: { include: [c_kernel] } }, @@ -242,14 +242,14 @@ function testECS() { em.defineSystem({ name: 'frameloop', actions: [ - { text: TEMPLATE1, event: 'start', select: 'once', query: { include: [c_kernel] }, + { text: TEMPLATE1, event: 'start', select: 'with', query: { include: [c_kernel] }, emits: ['preframe', 'kernel', 'postframe'] } ] }) em.defineSystem({ name: 'SetHorizPos', actions: [ - { text: SETHORIZPOS, event: 'SetHorizPos', select: 'once', query: { include: [c_xpos] } }, + { text: SETHORIZPOS, event: 'SetHorizPos', select: 'with', query: { include: [c_xpos] } }, ] }); diff --git a/src/worker/tools/ecs.ts b/src/worker/tools/ecs.ts index 3600ae1a..4d2de328 100644 --- a/src/worker/tools/ecs.ts +++ b/src/worker/tools/ecs.ts @@ -11,6 +11,7 @@ export function assembleECS(step: BuildStep): BuildStepResult { return getWorkFileAsString(path); } gatherFiles(step, { mainFilePath: "main.ecs" }); + if (step.mainfile) em.mainPath = step.path; var destpath = step.prefix + '.ca65'; if (staleFiles(step, [destpath])) { let code = getWorkFileAsString(step.path);