This commit is contained in:
dutchie031
2026-06-15 19:51:59 +02:00
parent ba292ad7aa
commit 16e76d96d3
8 changed files with 397 additions and 281 deletions
+320
View File
@@ -0,0 +1,320 @@
import * as fs from 'fs';
import * as path from 'path';
import { CompilationError } from './CompilationError';
/*
Block types.
Require is a special case as it's technically a function call, but it's a special use case.
*/
export enum BlockType {
Line,
Function,
If,
While,
For,
Do,
Return,
Require,
File
}
export abstract class CodeBlock {
protected childBlocks: CodeBlock[] = [];
private parentBlock?: CodeBlock;
private blockType: BlockType;
public readonly sourceLineNumber?: number;
constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock){
this.sourceLineNumber = sourceLineNumber;
this.parentBlock = parentBlock;
this.blockType = blockType;
}
public getChildren(): CodeBlock[] {
return this.childBlocks;
}
abstract toLines(): string[];
getParentBlock(): CodeBlock | undefined {
return this.parentBlock;
}
static createFromFile(luaFilePath: string, onError?: (error: CompilationError) => void): LuaFile {
if (!fs.existsSync(luaFilePath)) {
throw new Error(`File not found: ${luaFilePath}`);
}
const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
let leftCursor = 0;
let lineCounter = 0;
const file : LuaFile = new LuaFile();
let currentBlock : CodeBlock = file;
let currentWord = '';
let currentBlockString = '';
while(leftCursor < fileContent.length){
const currentChar : string = fileContent[leftCursor];
currentWord += currentChar;
currentBlockString += currentChar;
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
if(currentChar === '\n'){
lineCounter++;
}
if(currentChar === '-' && nextChar === '-'){
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
if(nextNextChar === '['){
// Multiline comment, skip to closing ]]
leftCursor += 3; // Skip the --[
while(leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')){
if(fileContent[leftCursor] === '\n'){
lineCounter++;
}
leftCursor++;
}
leftCursor += 2; // Skip the closing ]]
} else {
// Comment line, skip to end of line
while(leftCursor < fileContent.length && fileContent[leftCursor] !== '\n'){
leftCursor++;
}
if(fileContent[leftCursor] === '\n'){
lineCounter++;
}
}
currentWord = '';
continue;
}
if(currentChar === '"' || currentChar === "'"){
// String literal, skip to closing quote
const quoteType = currentChar;
leftCursor++;
while(leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType){
// Add string but don't process it for keywords
currentBlockString += fileContent[leftCursor];
leftCursor++;
}
currentBlockString += quoteType; // Add the closing quote
leftCursor++; // Skip the closing quote
currentWord = '';
continue;
}
if(currentChar === "\n") {
// Process line
currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace
const lineBlock = new LineBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
if(currentBlock.blockType === BlockType.Return){
// TODO: multi line return blocks
//Return blocks don't end on end, but on a new line
currentBlock = currentBlock.getParentBlock()!;
}
}
if(currentChar === ")" && currentBlock.blockType === BlockType.Require){
currentBlock = currentBlock.getParentBlock()!;
}
if(nextChar.trim() === '' || nextChar === '('){
// End of a word, check for keywords
const trimmedWord = currentWord.trim();
let blockToAdd : CodeBlock | null = null;
if(trimmedWord === 'if'){
blockToAdd = new IfBlock(lineCounter, currentBlock);
}
else if(trimmedWord === 'while'){
blockToAdd = new WhileBlock(lineCounter, currentBlock);
}
else if(trimmedWord === 'for'){
blockToAdd = new ForBlock(lineCounter, currentBlock);
}
else if(trimmedWord === 'do'){
blockToAdd = new DoBlock(lineCounter, currentBlock);
}
else if(trimmedWord === 'return'){
blockToAdd = new ReturnBlock(lineCounter, currentBlock);
}
else if(trimmedWord === 'function') {
blockToAdd = new FunctionBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'require') {
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
} else if(trimmedWord === 'end'){
const parent = currentBlock.getParentBlock();
if(!parent){
onError?.({
filePath: luaFilePath,
line: fileContent.substring(0, leftCursor).split('\n').length - 1,
message: "Unexpected 'end' without matching block start"
});
}
currentBlock = currentBlock.getParentBlock()!
}
if(blockToAdd){
currentBlock.childBlocks.push(blockToAdd);
currentBlock = blockToAdd;
}
currentWord = '';
}
leftCursor++;
}
return file;
}
}
export class LineBlock extends CodeBlock {
constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock){
super(sourceLineNumber, BlockType.Line, parent);
}
toLines(): string[] {
return [this.line];
}
}
export class IfBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.If, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class WhileBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.While, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class ForBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.For, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class LuaFile extends CodeBlock {
constructor() {
super(0, BlockType.File);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class DoBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.Do, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class ReturnBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.Return, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class FunctionBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){
super(sourceLineNumber, BlockType.Function, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
}
export class RequireBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number){
super(sourceLineNumber, BlockType.Require, parent);
}
toLines(): string[] {
const lines: string[] = [];
for(const child of this.childBlocks){
lines.push(...child.toLines());
}
return lines;
}
getRequiredString(): string {
if(this.childBlocks.length === 0){
return '';
}
const firstChild = this.childBlocks[0];
if(firstChild instanceof LineBlock){
return firstChild.toLines()[0].trim().replace(/['"]/g, '');
}
return '';
}
}