mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-11-26 10:49:17 +00:00
ecs: {{%props}}, ecs errors w/ $loc
This commit is contained in:
parent
249d4735eb
commit
c54b6a1150
@ -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,9 +237,8 @@ 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.`)
|
||||
@ -238,9 +249,6 @@ export class ECSCompiler extends Tokenizer {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user