1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-06-28 19:29:34 +00:00

ecs: {{%props}}, ecs errors w/ $loc

This commit is contained in:
Steven Hugg 2022-01-30 10:48:56 -06:00
parent 249d4735eb
commit c54b6a1150
4 changed files with 93 additions and 67 deletions

View File

@ -1,5 +1,6 @@
import { Tokenizer, TokenType } from "../tokenizer"; 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"; import { Action, ArrayType, ComponentType, DataField, DataType, DataValue, Dialect_CA65, Entity, EntityArchetype, EntityManager, EntityScope, IntType, Query, RefType, SelectType, SourceFileExport, System } from "./ecs";
export enum ECSTokenType { export enum ECSTokenType {
@ -18,7 +19,6 @@ export class ECSCompiler extends Tokenizer {
super(); super();
//this.includeEOL = true; //this.includeEOL = true;
this.setTokenRules([ this.setTokenRules([
{ type: TokenType.Ident, regex: /[A-Za-z_][A-Za-z0-9_]*/ },
{ type: TokenType.CodeFragment, regex: /---/ }, { type: TokenType.CodeFragment, regex: /---/ },
{ type: ECSTokenType.Ellipsis, regex: /\.\./ }, { type: ECSTokenType.Ellipsis, regex: /\.\./ },
{ type: ECSTokenType.Operator, 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: /[-]?0x[A-Fa-f0-9]+/ },
{ type: ECSTokenType.Integer, regex: /[-]?\$[A-Fa-f0-9]+/ }, { type: ECSTokenType.Integer, regex: /[-]?\$[A-Fa-f0-9]+/ },
{ type: ECSTokenType.Integer, regex: /[-]?\d+/ }, { type: ECSTokenType.Integer, regex: /[-]?\d+/ },
{ type: TokenType.Ident, regex: /[A-Za-z_][A-Za-z0-9_]*/ },
{ type: TokenType.Ignore, regex: /\s+/ }, { type: TokenType.Ignore, regex: /\s+/ },
]); ]);
this.errorOnCatchAll = true; this.errorOnCatchAll = true;
} }
annotate<T extends SourceLocated>(fn: () => T) {
let tok = this.peekToken();
let obj = fn();
if (obj) (obj as SourceLocated).$loc = tok.$loc;
return obj;
}
parseFile(text: string, path: string) { parseFile(text: string, path: string) {
this.tokenizeFile(text, path); this.tokenizeFile(text, path);
while (!this.isEOF()) { while (!this.isEOF()) {
this.parseTopLevel(); this.annotate(() => this.parseTopLevel());
} }
} }
@ -137,9 +145,10 @@ export class ECSCompiler extends Tokenizer {
let actions: Action[] = []; let actions: Action[] = [];
let system: System = { name, actions }; let system: System = { name, actions };
let cmd; let cmd;
while ((cmd = this.consumeToken().str) != 'end') { while ((cmd = this.expectTokens(['on','locals','end']).str) != 'end') {
if (cmd == 'on') { if (cmd == 'on') {
actions.push(this.parseAction()); let action = this.annotate(() => this.parseAction());
actions.push(action);
} else if (cmd == 'locals') { } else if (cmd == 'locals') {
system.tempbytes = this.expectInteger(); system.tempbytes = this.expectInteger();
} else { } else {
@ -150,6 +159,7 @@ export class ECSCompiler extends Tokenizer {
} }
parseAction(): Action { parseAction(): Action {
// TODO: unused events?
let event = this.expectIdent().str; let event = this.expectIdent().str;
this.expectToken('do'); this.expectToken('do');
let select = this.expectTokens(['once', 'foreach', 'source', 'join']).str as SelectType; // TODO: type check? 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; let limit;
if (this.peekToken().str == 'limit') { if (this.peekToken().str == 'limit') {
this.consumeToken(); this.consumeToken();
if (!['foreach', 'join'].includes(select)) this.compileError(`A "${select}" query can't include a limit.`);
limit = this.expectInteger(); limit = this.expectInteger();
} }
if (this.peekToken().str == 'emit') { if (this.peekToken().str == 'emit') {
@ -205,11 +216,12 @@ export class ECSCompiler extends Tokenizer {
let scope = this.em.newScope(name, this.currentScope); let scope = this.em.newScope(name, this.currentScope);
this.currentScope = scope; this.currentScope = scope;
let cmd; let cmd;
while ((cmd = this.consumeToken().str) != 'end') { while ((cmd = this.expectTokens(['entity', 'comment', 'end']).str) != 'end') {
if (cmd == 'entity') { if (cmd == 'entity') {
this.parseEntity(); this.annotate(() => this.parseEntity());
} else { }
this.compileError(`Unexpected scope keyword: ${cmd}`); if (cmd == 'comment') {
this.expectTokenTypes([TokenType.CodeFragment]);
} }
} }
this.currentScope = scope.parent; this.currentScope = scope.parent;
@ -225,22 +237,18 @@ export class ECSCompiler extends Tokenizer {
let e = this.currentScope.newEntity(etype); let e = this.currentScope.newEntity(etype);
e.name = name; e.name = name;
let cmd; let cmd;
while ((cmd = this.consumeToken().str) != 'end') { while ((cmd = this.expectTokens(['const', 'init', 'end']).str) != 'end') {
// TODO: check data types // TODO: check data types
if (cmd == 'const' || cmd == 'init') { let name = this.expectIdent().str;
let name = this.expectIdent().str; let comps = this.em.componentsWithFieldName([{etype: e.etype, cmatch:e.etype.components}], name);
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 == 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.`)
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);
let field = comps[0].fields.find(f => f.name == name); if (!field) this.internalError();
if (!field) this.internalError(); this.expectToken('=');
this.expectToken('='); let value = this.parseDataValue(field);
let value = this.parseDataValue(field); if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value);
if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value); if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value);
if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value);
} else {
this.compileError(`Unexpected scope keyword: ${cmd}`);
}
} }
return e; return e;
} }

View File

@ -53,7 +53,7 @@ function mkscopesymbol(s: EntityScope, c: ComponentType, fieldName: string) {
return s.name + '_' + c.name + '_' + fieldName; return s.name + '_' + c.name + '_' + fieldName;
} }
export interface Entity { export interface Entity extends SourceLocated {
id: number; id: number;
name?: string; name?: string;
etype: EntityArchetype; etype: EntityArchetype;
@ -71,7 +71,7 @@ export interface EntityArchetype {
components: ComponentType[]; components: ComponentType[];
} }
export interface ComponentType { export interface ComponentType extends SourceLocated {
name: string; name: string;
fields: DataField[]; fields: DataField[];
optional?: boolean; optional?: boolean;
@ -84,13 +84,13 @@ export interface Query {
updates?: string[]; updates?: string[];
} }
export interface System { export interface System extends SourceLocated {
name: string; name: string;
actions: Action[]; actions: Action[];
tempbytes?: number; tempbytes?: number;
} }
export interface Action { export interface Action extends SourceLocated {
text: string; text: string;
event: string; event: string;
select: SelectType select: SelectType
@ -158,26 +158,28 @@ export class Dialect_CA65 {
readonly ASM_ITERATE_EACH = ` readonly ASM_ITERATE_EACH = `
ldx #0 ldx #0
@__each: @__each:
{{code}} {{%code}}
inx inx
cpx #{{ecount}} cpx #{{%ecount}}
bne @__each bne @__each
@__exit:
`; `;
readonly ASM_ITERATE_JOIN = ` readonly ASM_ITERATE_JOIN = `
ldy #0 ldy #0
@__each: @__each:
ldx {{joinfield}},y ldx {{%joinfield}},y
{{code}} {{%code}}
iny iny
cpy #{{ecount}} cpy #{{%ecount}}
bne @__each bne @__each
@__exit:
`; `;
readonly INIT_FROM_ARRAY = ` readonly INIT_FROM_ARRAY = `
ldy #{{nbytes}} ldy #{{%nbytes}}
: lda {{src}}-1,y : lda {{%src}}-1,y
sta {{dest}}-1,y sta {{%dest}}-1,y
dey dey
bne :- bne :-
` `
@ -337,7 +339,8 @@ function getPackedFieldSize(f: DataType, constValue?: DataValue): number {
return 0; return 0;
} }
export class EntityScope { export class EntityScope implements SourceLocated {
$loc: SourceLocation;
childScopes: EntityScope[] = []; childScopes: EntityScope[] = [];
entities: Entity[] = []; entities: Entity[] = [];
bss = new Segment(); bss = new Segment();
@ -430,10 +433,11 @@ export class EntityScope {
hasComponent(ctype: ComponentType) { hasComponent(ctype: ComponentType) {
return this.componentsInScope.has(ctype.name); 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')); let refs = Array.from(this.iterateArchetypeFields(atypes, (c,f) => f.dtype == 'ref'));
if (refs.length == 0) throw new ECSError(`cannot find join fields`); // TODO: better error message
if (refs.length > 1) throw new ECSError(`cannot join multiple fields`); 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 // TODO: check to make sure join works
return refs[0]; // TODO return refs[0]; // TODO
/* TODO /* TODO
@ -546,23 +550,23 @@ export class EntityScope {
let bufofs = this.rodata.allocateInitData(bufsym, initbytes); let bufofs = this.rodata.allocateInitData(bufsym, initbytes);
let code = this.dialect.INIT_FROM_ARRAY; let code = this.dialect.INIT_FROM_ARRAY;
//TODO: function to repalce from dict? //TODO: function to repalce from dict?
code = code.replace('{{nbytes}}', initbytes.length.toString()) code = code.replace('{{%nbytes}}', initbytes.length.toString())
code = code.replace('{{src}}', bufsym); code = code.replace('{{%src}}', bufsym);
code = code.replace('{{dest}}', segment.getOriginSymbol()); code = code.replace('{{%dest}}', segment.getOriginSymbol());
return code; return code;
} }
setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) {
let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setConstValue"); let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setConstValue");
e.consts[mksymbol(component, fieldName)] = value; e.consts[mksymbol(component, fieldName)] = value;
if (this.em.symbols[mksymbol(component, fieldName)] == 'init') 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'; this.em.symbols[mksymbol(component, fieldName)] = 'const';
} }
setInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { setInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) {
let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setInitValue"); let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setInitValue");
e.inits[mkscopesymbol(this, component, fieldName)] = value; e.inits[mkscopesymbol(this, component, fieldName)] = value;
if (this.em.symbols[mksymbol(component, fieldName)] == 'const') 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'; this.em.symbols[mksymbol(component, fieldName)] = 'init';
} }
generateCodeForEvent(event: string): string { generateCodeForEvent(event: string): string {
@ -611,29 +615,43 @@ export class EntityScope {
} }
replaceCode(code: string, sys: System, action: Action): string { replaceCode(code: string, sys: System, action: Action): string {
const tag_re = /\{\{(.+?)\}\}/g; const tag_re = /\{\{(.+?)\}\}/g;
const label_re = /@(\w+)\b/g;
let label = `${sys.name}__${action.event}`; let label = `${sys.name}__${action.event}`;
let atypes = this.em.archetypesMatching(action.query); let atypes = this.em.archetypesMatching(action.query);
let entities = this.entitiesMatching(atypes); 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: detect cycles
// TODO: "source"? // TODO: "source"?
// TODO: what if only 1 item? // TODO: what if only 1 item?
let props : {[name: string] : string} = {};
if (action.select == 'foreach') { if (action.select == 'foreach') {
code = this.wrapCodeInLoop(code, action, entities); code = this.wrapCodeInLoop(code, action, entities);
} }
if (action.select == 'join' && action.join) { if (action.select == 'join' && action.join) {
let jtypes = this.em.archetypesMatching(action.join); let jtypes = this.em.archetypesMatching(action.join);
let jentities = this.entitiesMatching(jtypes); 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: what if only 1 item?
// TODO: should be able to access fields via Y reg
code = this.wrapCodeInLoop(code, action, entities, joinfield); code = this.wrapCodeInLoop(code, action, entities, joinfield);
atypes = jtypes; atypes = jtypes;
entities = jentities; 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 // 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 // replace {{...}} tags
return code.replace(tag_re, (entire, group: string) => { code = code.replace(tag_re, (entire, group: string) => {
let cmd = group.charAt(0); let cmd = group.charAt(0);
let rest = group.substring(1); let rest = group.substring(1);
switch (cmd) { switch (cmd) {
@ -642,7 +660,7 @@ export class EntityScope {
case '.': // auto label case '.': // auto label
case '@': // auto label case '@': // auto label
return `${label}_${rest}`; return `${label}_${rest}`;
case '$': // temp byte case '$': // temp byte (TODO: check to make sure not overflowing)
return `TEMP+${this.tempOffset}+${rest}`; return `TEMP+${this.tempOffset}+${rest}`;
case '=': case '=':
// TODO? // TODO?
@ -653,9 +671,12 @@ export class EntityScope {
case '^': // subroutine reference case '^': // subroutine reference
return this.includeSubroutine(rest); return this.includeSubroutine(rest);
default: 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 { includeSubroutine(symbol: string): string {
this.subroutines.add(symbol); this.subroutines.add(symbol);
@ -667,16 +688,7 @@ export class EntityScope {
// TODO: what if 0 or 1 entitites? // TODO: what if 0 or 1 entitites?
let s = this.dialect.ASM_ITERATE_EACH; let s = this.dialect.ASM_ITERATE_EACH;
if (joinfield) s = this.dialect.ASM_ITERATE_JOIN; if (joinfield) s = this.dialect.ASM_ITERATE_JOIN;
if (action.limit) { s = s.replace('{{%code}}', code);
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));
}
return s; return s;
} }
generateCodeForField(sys: System, action: Action, generateCodeForField(sys: System, action: Action,

View File

@ -101,9 +101,10 @@ export class Tokenizer {
// add token to list // add token to list
switch (rule.type) { switch (rule.type) {
case TokenType.CodeFragment: case TokenType.CodeFragment:
if (this.codeFragment) { // TODO: empty code fragment doesn't work
if (this.codeFragment != null) {
let codeLoc = mergeLocs(this.codeFragmentStart, loc); 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.codeFragmentStart = null;
this.codeFragment = null; this.codeFragment = null;
} else { } else {
@ -121,6 +122,7 @@ export class Tokenizer {
if (this.codeFragment == null) { if (this.codeFragment == null) {
this._pushToken({ str: s, type: rule.type, $loc: loc }); this._pushToken({ str: s, type: rule.type, $loc: loc });
} }
case TokenType.Comment:
case TokenType.Ignore: case TokenType.Ignore:
break; break;
} }

View File

@ -1,4 +1,5 @@
import { ECSCompiler } from "../../common/ecs/compiler"; import { ECSCompiler } from "../../common/ecs/compiler";
import { ECSError } from "../../common/ecs/ecs";
import { CompileError } from "../../common/tokenizer"; import { CompileError } from "../../common/tokenizer";
import { CodeListingMap } from "../../common/workertypes"; import { CodeListingMap } from "../../common/workertypes";
import { BuildStep, BuildStepResult, gatherFiles, getWorkFileAsString, putWorkFile, staleFiles } from "../workermain"; import { BuildStep, BuildStepResult, gatherFiles, getWorkFileAsString, putWorkFile, staleFiles } from "../workermain";
@ -11,17 +12,20 @@ export function assembleECS(step: BuildStep): BuildStepResult {
let code = getWorkFileAsString(step.path); let code = getWorkFileAsString(step.path);
try { try {
compiler.parseFile(code, step.path); compiler.parseFile(code, step.path);
let outtext = compiler.export().toString();
putWorkFile(destpath, outtext);
var listings: CodeListingMap = {};
listings[destpath] = {lines:[], text:outtext} // TODO
} catch (e) { } 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 }; return { errors: compiler.errors };
} else { } else {
throw e; throw e;
} }
} }
let outtext = compiler.export().toString();
putWorkFile(destpath, outtext);
var listings: CodeListingMap = {};
listings[destpath] = {lines:[], text:outtext} // TODO
} }
return { return {
nexttool: "ca65", nexttool: "ca65",