From c54b6a115063cf3df7f0d0ff5cb110c92e3ad98e Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 30 Jan 2022 10:48:56 -0600 Subject: [PATCH] ecs: {{%props}}, ecs errors w/ $loc --- src/common/ecs/compiler.ts | 54 ++++++++++++++---------- src/common/ecs/ecs.ts | 86 ++++++++++++++++++++++---------------- src/common/tokenizer.ts | 6 ++- src/worker/tools/ecs.ts | 14 ++++--- 4 files changed, 93 insertions(+), 67 deletions(-) diff --git a/src/common/ecs/compiler.ts b/src/common/ecs/compiler.ts index 1e06aa46..904e4348 100644 --- a/src/common/ecs/compiler.ts +++ b/src/common/ecs/compiler.ts @@ -1,5 +1,6 @@ import { 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"; export enum ECSTokenType { @@ -18,7 +19,6 @@ export class ECSCompiler extends Tokenizer { super(); //this.includeEOL = true; this.setTokenRules([ - { type: TokenType.Ident, regex: /[A-Za-z_][A-Za-z0-9_]*/ }, { type: TokenType.CodeFragment, regex: /---/ }, { type: ECSTokenType.Ellipsis, regex: /\.\./ }, { type: ECSTokenType.Operator, regex: /[#=,:(){}\[\]]/ }, @@ -26,15 +26,23 @@ export class ECSCompiler extends Tokenizer { { type: ECSTokenType.Integer, regex: /[-]?0x[A-Fa-f0-9]+/ }, { type: ECSTokenType.Integer, regex: /[-]?\$[A-Fa-f0-9]+/ }, { type: ECSTokenType.Integer, regex: /[-]?\d+/ }, + { type: TokenType.Ident, regex: /[A-Za-z_][A-Za-z0-9_]*/ }, { type: TokenType.Ignore, regex: /\s+/ }, ]); this.errorOnCatchAll = true; } + + annotate(fn: () => T) { + let tok = this.peekToken(); + let obj = fn(); + if (obj) (obj as SourceLocated).$loc = tok.$loc; + return obj; + } parseFile(text: string, path: string) { this.tokenizeFile(text, path); while (!this.isEOF()) { - this.parseTopLevel(); + this.annotate(() => this.parseTopLevel()); } } @@ -137,9 +145,10 @@ export class ECSCompiler extends Tokenizer { let actions: Action[] = []; let system: System = { name, actions }; let cmd; - while ((cmd = this.consumeToken().str) != 'end') { + while ((cmd = this.expectTokens(['on','locals','end']).str) != 'end') { if (cmd == 'on') { - actions.push(this.parseAction()); + let action = this.annotate(() => this.parseAction()); + actions.push(action); } else if (cmd == 'locals') { system.tempbytes = this.expectInteger(); } else { @@ -150,6 +159,7 @@ export class ECSCompiler extends Tokenizer { } parseAction(): Action { + // 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? @@ -159,6 +169,7 @@ export class ECSCompiler extends Tokenizer { let limit; if (this.peekToken().str == 'limit') { this.consumeToken(); + if (!['foreach', 'join'].includes(select)) this.compileError(`A "${select}" query can't include a limit.`); limit = this.expectInteger(); } if (this.peekToken().str == 'emit') { @@ -205,11 +216,12 @@ export class ECSCompiler extends Tokenizer { let scope = this.em.newScope(name, this.currentScope); this.currentScope = scope; let cmd; - while ((cmd = this.consumeToken().str) != 'end') { + while ((cmd = this.expectTokens(['entity', 'comment', 'end']).str) != 'end') { if (cmd == 'entity') { - this.parseEntity(); - } else { - this.compileError(`Unexpected scope keyword: ${cmd}`); + this.annotate(() => this.parseEntity()); + } + if (cmd == 'comment') { + this.expectTokenTypes([TokenType.CodeFragment]); } } this.currentScope = scope.parent; @@ -225,22 +237,18 @@ export class ECSCompiler extends Tokenizer { let e = this.currentScope.newEntity(etype); e.name = name; let cmd; - while ((cmd = this.consumeToken().str) != 'end') { + while ((cmd = this.expectTokens(['const', 'init', 'end']).str) != 'end') { // TODO: check data types - if (cmd == 'const' || cmd == 'init') { - let name = this.expectIdent().str; - let comps = this.em.componentsWithFieldName([{etype: e.etype, cmatch:e.etype.components}], name); - if (comps.length == 0) this.compileError(`I couldn't find a field named "${name}" for this entity.`) - if (comps.length > 1) this.compileError(`I found more than one field named "${name}" for this entity.`) - let field = comps[0].fields.find(f => f.name == name); - if (!field) this.internalError(); - this.expectToken('='); - let value = this.parseDataValue(field); - if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value); - if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value); - } else { - this.compileError(`Unexpected scope keyword: ${cmd}`); - } + let name = this.expectIdent().str; + let comps = this.em.componentsWithFieldName([{etype: e.etype, cmatch:e.etype.components}], name); + if (comps.length == 0) this.compileError(`I couldn't find a field named "${name}" for this entity.`) + if (comps.length > 1) this.compileError(`I found more than one field named "${name}" for this entity.`) + let field = comps[0].fields.find(f => f.name == name); + if (!field) this.internalError(); + this.expectToken('='); + let value = this.parseDataValue(field); + if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value); + if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value); } return e; } diff --git a/src/common/ecs/ecs.ts b/src/common/ecs/ecs.ts index caeee962..78c90de5 100644 --- a/src/common/ecs/ecs.ts +++ b/src/common/ecs/ecs.ts @@ -53,7 +53,7 @@ function mkscopesymbol(s: EntityScope, c: ComponentType, fieldName: string) { return s.name + '_' + c.name + '_' + fieldName; } -export interface Entity { +export interface Entity extends SourceLocated { id: number; name?: string; etype: EntityArchetype; @@ -71,7 +71,7 @@ export interface EntityArchetype { components: ComponentType[]; } -export interface ComponentType { +export interface ComponentType extends SourceLocated { name: string; fields: DataField[]; optional?: boolean; @@ -84,13 +84,13 @@ export interface Query { updates?: string[]; } -export interface System { +export interface System extends SourceLocated { name: string; actions: Action[]; tempbytes?: number; } -export interface Action { +export interface Action extends SourceLocated { text: string; event: string; select: SelectType @@ -158,26 +158,28 @@ export class Dialect_CA65 { readonly ASM_ITERATE_EACH = ` ldx #0 @__each: - {{code}} + {{%code}} inx - cpx #{{ecount}} + cpx #{{%ecount}} bne @__each +@__exit: `; readonly ASM_ITERATE_JOIN = ` ldy #0 @__each: - ldx {{joinfield}},y - {{code}} + ldx {{%joinfield}},y + {{%code}} iny - cpy #{{ecount}} + cpy #{{%ecount}} bne @__each +@__exit: `; readonly INIT_FROM_ARRAY = ` - ldy #{{nbytes}} -: lda {{src}}-1,y - sta {{dest}}-1,y + ldy #{{%nbytes}} +: lda {{%src}}-1,y + sta {{%dest}}-1,y dey bne :- ` @@ -337,7 +339,8 @@ function getPackedFieldSize(f: DataType, constValue?: DataValue): number { return 0; } -export class EntityScope { +export class EntityScope implements SourceLocated { + $loc: SourceLocation; childScopes: EntityScope[] = []; entities: Entity[] = []; bss = new Segment(); @@ -430,10 +433,11 @@ export class EntityScope { hasComponent(ctype: ComponentType) { return this.componentsInScope.has(ctype.name); } - getJoinField(atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]) : ComponentFieldPair { + getJoinField(action: Action, atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]) : ComponentFieldPair { let refs = Array.from(this.iterateArchetypeFields(atypes, (c,f) => f.dtype == 'ref')); - if (refs.length == 0) throw new ECSError(`cannot find join fields`); - if (refs.length > 1) throw new ECSError(`cannot join multiple fields`); + // TODO: better error message + 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 @@ -546,23 +550,23 @@ export class EntityScope { let bufofs = this.rodata.allocateInitData(bufsym, initbytes); let code = this.dialect.INIT_FROM_ARRAY; //TODO: function to repalce from dict? - code = code.replace('{{nbytes}}', initbytes.length.toString()) - code = code.replace('{{src}}', bufsym); - code = code.replace('{{dest}}', segment.getOriginSymbol()); + code = code.replace('{{%nbytes}}', initbytes.length.toString()) + code = code.replace('{{%src}}', bufsym); + code = code.replace('{{%dest}}', segment.getOriginSymbol()); return code; } setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setConstValue"); e.consts[mksymbol(component, fieldName)] = value; if (this.em.symbols[mksymbol(component, fieldName)] == 'init') - throw new ECSError(`Can't mix const and init values for a component field`); + throw new ECSError(`Can't mix const and init values for a component field`, e); this.em.symbols[mksymbol(component, fieldName)] = 'const'; } setInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setInitValue"); e.inits[mkscopesymbol(this, component, fieldName)] = value; if (this.em.symbols[mksymbol(component, fieldName)] == 'const') - throw new ECSError(`Can't mix const and init values for a component field`); + throw new ECSError(`Can't mix const and init values for a component field`, e); this.em.symbols[mksymbol(component, fieldName)] = 'init'; } generateCodeForEvent(event: string): string { @@ -611,29 +615,43 @@ export class EntityScope { } replaceCode(code: string, sys: System, action: Action): string { const tag_re = /\{\{(.+?)\}\}/g; + const label_re = /@(\w+)\b/g; + let label = `${sys.name}__${action.event}`; let atypes = this.em.archetypesMatching(action.query); let entities = this.entitiesMatching(atypes); + if (entities.length == 0) throw new ECSError(`action ${label} doesn't match any entities`, action); // TODO // TODO: detect cycles // TODO: "source"? // TODO: what if only 1 item? + let props : {[name: string] : string} = {}; if (action.select == 'foreach') { code = this.wrapCodeInLoop(code, action, entities); } if (action.select == 'join' && action.join) { let jtypes = this.em.archetypesMatching(action.join); let jentities = this.entitiesMatching(jtypes); - let joinfield = this.getJoinField(atypes, jtypes); + let joinfield = this.getJoinField(action, atypes, jtypes); // TODO: what if only 1 item? + // TODO: should be able to access fields via Y reg code = this.wrapCodeInLoop(code, action, entities, joinfield); atypes = jtypes; entities = jentities; + props['%joinfield'] = this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0); } - if (entities.length == 0) throw new ECSError(`action ${label} doesn't match any entities`); + props['%efullcount'] = entities.length.toString(); + if (action.limit) { + entities = entities.slice(0, action.limit); + } + if (entities.length == 0) throw new ECSError(`action ${label} doesn't match any entities`); // TODO + // define properties + props['%elo'] = entities[0].id.toString(); + props['%ehi'] = entities[entities.length - 1].id.toString(); + props['%ecount'] = entities.length.toString(); // replace @labels - code = code.replace(/@(\w+)\b/g, (s: string, a: string) => `${label}__${a}`); + code = code.replace(label_re, (s: string, a: string) => `${label}__${a}`); // replace {{...}} tags - return code.replace(tag_re, (entire, group: string) => { + code = code.replace(tag_re, (entire, group: string) => { let cmd = group.charAt(0); let rest = group.substring(1); switch (cmd) { @@ -642,7 +660,7 @@ export class EntityScope { case '.': // auto label case '@': // auto label return `${label}_${rest}`; - case '$': // temp byte + case '$': // temp byte (TODO: check to make sure not overflowing) return `TEMP+${this.tempOffset}+${rest}`; case '=': // TODO? @@ -653,9 +671,12 @@ export class EntityScope { case '^': // subroutine reference return this.includeSubroutine(rest); default: - throw new ECSError(`unrecognized command ${cmd} in ${entire}`); + let value = props[group]; + if (value) return value; + else throw new ECSError(`unrecognized command {{${group}}} in ${entire}`); } }); + return code; } includeSubroutine(symbol: string): string { this.subroutines.add(symbol); @@ -667,16 +688,7 @@ export class EntityScope { // TODO: what if 0 or 1 entitites? let s = this.dialect.ASM_ITERATE_EACH; if (joinfield) s = this.dialect.ASM_ITERATE_JOIN; - if (action.limit) { - ents = ents.slice(0, action.limit); - } - s = s.replace('{{elo}}', () => ents[0].id.toString()); - s = s.replace('{{ehi}}', () => ents[ents.length - 1].id.toString()); - s = s.replace('{{ecount}}', () => ents.length.toString()); - s = s.replace('{{code}}', code); - if (joinfield) { - s = s.replace('{{joinfield}}', () => this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0)); - } + s = s.replace('{{%code}}', code); return s; } generateCodeForField(sys: System, action: Action, diff --git a/src/common/tokenizer.ts b/src/common/tokenizer.ts index 1eb6d49d..0615b2e5 100644 --- a/src/common/tokenizer.ts +++ b/src/common/tokenizer.ts @@ -101,9 +101,10 @@ export class Tokenizer { // add token to list switch (rule.type) { case TokenType.CodeFragment: - if (this.codeFragment) { + // TODO: empty code fragment doesn't work + if (this.codeFragment != null) { let codeLoc = mergeLocs(this.codeFragmentStart, loc); - this._pushToken({ str: this.codeFragment, type: rule.type, $loc: codeLoc }); //TODO: merge start/end + this._pushToken({ str: this.codeFragment, type: rule.type, $loc: codeLoc }); this.codeFragmentStart = null; this.codeFragment = null; } else { @@ -121,6 +122,7 @@ export class Tokenizer { if (this.codeFragment == null) { this._pushToken({ str: s, type: rule.type, $loc: loc }); } + case TokenType.Comment: case TokenType.Ignore: break; } diff --git a/src/worker/tools/ecs.ts b/src/worker/tools/ecs.ts index c130d8a5..37a1eef7 100644 --- a/src/worker/tools/ecs.ts +++ b/src/worker/tools/ecs.ts @@ -1,4 +1,5 @@ import { ECSCompiler } from "../../common/ecs/compiler"; +import { ECSError } from "../../common/ecs/ecs"; import { CompileError } from "../../common/tokenizer"; import { CodeListingMap } from "../../common/workertypes"; import { BuildStep, BuildStepResult, gatherFiles, getWorkFileAsString, putWorkFile, staleFiles } from "../workermain"; @@ -11,17 +12,20 @@ export function assembleECS(step: BuildStep): BuildStepResult { let code = getWorkFileAsString(step.path); try { compiler.parseFile(code, step.path); + let outtext = compiler.export().toString(); + putWorkFile(destpath, outtext); + var listings: CodeListingMap = {}; + listings[destpath] = {lines:[], text:outtext} // TODO } catch (e) { - if (e instanceof CompileError) { + if (e instanceof ECSError) { + compiler.addError(e.message, e.$loc); + return { errors: compiler.errors }; + } else if (e instanceof CompileError) { return { errors: compiler.errors }; } else { throw e; } } - let outtext = compiler.export().toString(); - putWorkFile(destpath, outtext); - var listings: CodeListingMap = {}; - listings[destpath] = {lines:[], text:outtext} // TODO } return { nexttool: "ca65",