1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-11-29 14:51:17 +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 { 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<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) {
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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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",