2022-01-28 17:22:59 +00:00
|
|
|
|
|
|
|
import { Tokenizer, TokenType } from "../tokenizer";
|
|
|
|
import { Action, ArrayType, ComponentType, DataField, DataType, DataValue, Dialect_CA65, Entity, EntityArchetype, EntityManager, EntityScope, IntType, Query, RefType, SelectType, SourceFileExport, System } from "./ecs";
|
|
|
|
|
|
|
|
export enum ECSTokenType {
|
|
|
|
Ellipsis = 'ellipsis',
|
|
|
|
Operator = 'delimiter',
|
|
|
|
QuotedString = 'quoted-string',
|
|
|
|
Integer = 'integer',
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ECSCompiler extends Tokenizer {
|
|
|
|
|
|
|
|
em = new EntityManager(new Dialect_CA65()); // TODO
|
|
|
|
currentScope: EntityScope | null = null;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
//this.includeEOL = true;
|
|
|
|
this.setTokenRules([
|
2022-01-29 03:13:33 +00:00
|
|
|
{ type: TokenType.Ident, regex: /[A-Za-z_][A-Za-z0-9_]*/ },
|
2022-01-28 17:22:59 +00:00
|
|
|
{ type: TokenType.CodeFragment, regex: /---/ },
|
|
|
|
{ type: ECSTokenType.Ellipsis, regex: /\.\./ },
|
2022-01-29 03:13:33 +00:00
|
|
|
{ type: ECSTokenType.Operator, regex: /[#=,:(){}\[\]]/ },
|
2022-01-28 17:22:59 +00:00
|
|
|
{ type: ECSTokenType.QuotedString, regex: /".*?"/ },
|
2022-01-29 03:13:33 +00:00
|
|
|
{ type: ECSTokenType.Integer, regex: /[-]?0x[A-Fa-f0-9]+/ },
|
|
|
|
{ type: ECSTokenType.Integer, regex: /[-]?\$[A-Fa-f0-9]+/ },
|
2022-01-28 17:22:59 +00:00
|
|
|
{ type: ECSTokenType.Integer, regex: /[-]?\d+/ },
|
|
|
|
{ type: TokenType.Ignore, regex: /\s+/ },
|
|
|
|
]);
|
|
|
|
this.errorOnCatchAll = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseFile(text: string, path: string) {
|
|
|
|
this.tokenizeFile(text, path);
|
|
|
|
while (!this.isEOF()) {
|
|
|
|
this.parseTopLevel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parseTopLevel() {
|
|
|
|
//this.skipBlankLines();
|
|
|
|
let tok = this.expectTokens(['component', 'system', 'scope', 'comment']);
|
|
|
|
if (tok.str == 'component') {
|
|
|
|
return this.em.defineComponent(this.parseComponentDefinition());
|
|
|
|
}
|
|
|
|
if (tok.str == 'system') {
|
|
|
|
return this.em.defineSystem(this.parseSystem());
|
|
|
|
}
|
|
|
|
if (tok.str == 'scope') {
|
|
|
|
return this.parseScope();
|
|
|
|
}
|
|
|
|
if (tok.str == 'comment') {
|
|
|
|
this.expectTokenTypes([TokenType.CodeFragment]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.compileError(`Unexpected top-level keyword: ${tok.str}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
parseComponentDefinition(): ComponentType {
|
|
|
|
let name = this.expectIdent().str;
|
|
|
|
let fields = [];
|
|
|
|
while (this.peekToken().str != 'end') {
|
|
|
|
fields.push(this.parseComponentField());
|
|
|
|
}
|
|
|
|
this.expectToken('end');
|
|
|
|
return { name, fields };
|
|
|
|
}
|
|
|
|
|
|
|
|
parseComponentField(): DataField {
|
|
|
|
let name = this.expectIdent();
|
|
|
|
this.expectToken(':');
|
|
|
|
let type = this.parseDataType();
|
|
|
|
return { name: name.str, ...type };
|
|
|
|
}
|
|
|
|
|
|
|
|
parseDataType(): DataType {
|
|
|
|
if (this.peekToken().type == 'integer') {
|
|
|
|
let lo = this.expectInteger();
|
|
|
|
this.expectToken('..');
|
|
|
|
let hi = this.expectInteger();
|
|
|
|
return { dtype: 'int', lo, hi } as IntType;
|
|
|
|
}
|
|
|
|
if (this.peekToken().str == '[') {
|
|
|
|
return { dtype: 'ref', query: this.parseQuery() } as RefType;
|
|
|
|
}
|
|
|
|
if (this.peekToken().str == 'array') {
|
|
|
|
this.expectToken('array');
|
|
|
|
this.expectToken('of');
|
|
|
|
return { dtype: 'array', elem: this.parseDataType() } as ArrayType;
|
|
|
|
}
|
|
|
|
this.compileError(`Unknown data type`); // TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
parseDataValue() : DataValue {
|
2022-01-29 03:13:33 +00:00
|
|
|
let tok = this.peekToken();
|
|
|
|
if (tok.type == 'integer') {
|
2022-01-28 17:22:59 +00:00
|
|
|
return this.expectInteger();
|
|
|
|
}
|
2022-01-29 03:13:33 +00:00
|
|
|
if (tok.str == '[') {
|
|
|
|
// TODO: 16-bit?
|
|
|
|
return new Uint8Array(this.parseDataArray());
|
|
|
|
}
|
|
|
|
if (tok.str == '#') {
|
|
|
|
let e = this.parseEntityRef();
|
|
|
|
// TODO: entity ref types by query?
|
|
|
|
return e.id;
|
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
this.compileError(`Unknown data value`); // TODO
|
|
|
|
}
|
|
|
|
|
2022-01-29 03:13:33 +00:00
|
|
|
parseDataArray() {
|
|
|
|
this.expectToken('[');
|
|
|
|
let arr = this.parseList(this.expectInteger, ',');
|
|
|
|
this.expectToken(']');
|
|
|
|
return arr;
|
|
|
|
}
|
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
expectInteger(): number {
|
2022-01-29 03:13:33 +00:00
|
|
|
let s = this.consumeToken().str;
|
|
|
|
if (s.startsWith('$')) s = '0x' + s.substring(1);
|
|
|
|
let i = parseInt(s);
|
2022-01-28 17:22:59 +00:00
|
|
|
if (isNaN(i)) this.compileError('There should be an integer here.');
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseSystem(): System {
|
|
|
|
let name = this.expectIdent().str;
|
|
|
|
let actions: Action[] = [];
|
|
|
|
let system: System = { name, actions };
|
|
|
|
let cmd;
|
|
|
|
while ((cmd = this.consumeToken().str) != 'end') {
|
|
|
|
if (cmd == 'on') {
|
|
|
|
actions.push(this.parseAction());
|
|
|
|
} else if (cmd == 'locals') {
|
|
|
|
system.tempbytes = this.expectInteger();
|
|
|
|
} else {
|
|
|
|
this.compileError(`Unexpected system keyword: ${cmd}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return system;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseAction(): Action {
|
|
|
|
let event = this.expectIdent().str;
|
|
|
|
this.expectToken('do');
|
2022-01-29 03:13:33 +00:00
|
|
|
let select = this.expectTokens(['once', 'each', 'source']).str as SelectType; // TODO: type check?
|
2022-01-28 17:22:59 +00:00
|
|
|
let query = this.parseQuery();
|
2022-01-29 03:13:33 +00:00
|
|
|
let emits;
|
|
|
|
if (this.peekToken().str == 'emit') {
|
|
|
|
this.consumeToken();
|
|
|
|
this.expectToken('(');
|
|
|
|
emits = this.parseEventList();
|
|
|
|
this.expectToken(')');
|
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
let text = this.parseCode();
|
|
|
|
return { text, event, query, select };
|
|
|
|
}
|
2022-01-29 03:13:33 +00:00
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
parseQuery() {
|
|
|
|
let q: Query = { include: [] };
|
|
|
|
this.expectToken('[');
|
|
|
|
q.include = this.parseList(this.parseComponentRef, ',').map(c => c.name);
|
|
|
|
// TODO: other params
|
|
|
|
this.expectToken(']');
|
|
|
|
return q;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseEvent() {
|
|
|
|
return this.expectIdent().str;
|
|
|
|
}
|
|
|
|
|
2022-01-29 03:13:33 +00:00
|
|
|
parseEventList() {
|
|
|
|
return this.parseList(this.parseEvent, ",");
|
|
|
|
}
|
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
parseCode(): string {
|
|
|
|
return this.expectTokenTypes([TokenType.CodeFragment]).str;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseScope() : EntityScope {
|
|
|
|
let name = this.expectIdent().str;
|
|
|
|
let scope = this.em.newScope(name, this.currentScope);
|
|
|
|
this.currentScope = scope;
|
|
|
|
let cmd;
|
|
|
|
while ((cmd = this.consumeToken().str) != 'end') {
|
|
|
|
if (cmd == 'entity') {
|
|
|
|
this.parseEntity();
|
|
|
|
} else {
|
|
|
|
this.compileError(`Unexpected scope keyword: ${cmd}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.currentScope = scope.parent;
|
|
|
|
return scope;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseEntity() : Entity {
|
|
|
|
let name = '';
|
|
|
|
if (this.peekToken().type == TokenType.Ident) {
|
|
|
|
name = this.expectIdent().str;
|
|
|
|
}
|
|
|
|
let etype = this.parseEntityArchetype();
|
|
|
|
let e = this.currentScope.newEntity(etype);
|
|
|
|
e.name = name;
|
|
|
|
let cmd;
|
|
|
|
while ((cmd = this.consumeToken().str) != 'end') {
|
|
|
|
// TODO: check data types
|
2022-01-29 03:13:33 +00:00
|
|
|
if (cmd == 'const' || cmd == 'init') {
|
2022-01-28 17:22:59 +00:00
|
|
|
let name = this.expectIdent().str;
|
|
|
|
this.expectToken('=');
|
2022-01-29 03:13:33 +00:00
|
|
|
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 value = this.parseDataValue();
|
|
|
|
if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value);
|
|
|
|
if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value);
|
2022-01-28 17:22:59 +00:00
|
|
|
} else {
|
|
|
|
this.compileError(`Unexpected scope keyword: ${cmd}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return e;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseEntityArchetype() : EntityArchetype {
|
|
|
|
this.expectToken('[');
|
|
|
|
let components = this.parseList(this.parseComponentRef, ',');
|
|
|
|
this.expectToken(']');
|
|
|
|
return {components};
|
|
|
|
}
|
|
|
|
|
|
|
|
parseComponentRef() : ComponentType {
|
|
|
|
let name = this.expectIdent().str;
|
|
|
|
let cref = this.em.getComponentByName(name);
|
|
|
|
if (!cref) this.compileError(`I couldn't find a component named "${name}".`)
|
|
|
|
return cref;
|
|
|
|
}
|
|
|
|
|
2022-01-29 03:13:33 +00:00
|
|
|
parseEntityRef() : Entity {
|
|
|
|
this.expectToken('#');
|
|
|
|
let name = this.expectIdent().str;
|
|
|
|
let eref = this.currentScope.entities.find(e => e.name == name);
|
|
|
|
if (!eref) this.compileError(`I couldn't find an entity named "${name}" in this scope.`)
|
|
|
|
return eref;
|
|
|
|
}
|
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
exportToFile(src: SourceFileExport) {
|
|
|
|
for (let scope of Object.values(this.em.scopes)) {
|
|
|
|
scope.analyzeEntities();
|
|
|
|
scope.generateCode();
|
|
|
|
scope.dump(src);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|