This commit is contained in:
dutchie031
2026-07-03 13:26:01 +02:00
parent 16e76d96d3
commit df79538491
4 changed files with 188 additions and 77 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js", "${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
"${workspaceFolder}/compiler/dist/**/*.js" "${workspaceFolder}/compiler/dist/**/*.js"
], ],
"preLaunchTask": "compile" "preLaunchTask": "watch"
} }
] ]
} }
+1
View File
@@ -24,6 +24,7 @@
"reveal": "never" "reveal": "never"
}, },
"group": "build", "group": "build",
"problemMatcher": "$tsc-watch",
"options": { "options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
} }
+175 -69
View File
@@ -8,7 +8,7 @@ import { CompilationError } from './CompilationError';
Require is a special case as it's technically a function call, but it's a special use case. Require is a special case as it's technically a function call, but it's a special use case.
*/ */
export enum BlockType { export enum BlockType {
Line, CodeTextBlock,
Function, Function,
If, If,
While, While,
@@ -16,9 +16,15 @@ export enum BlockType {
Do, Do,
Return, Return,
Require, Require,
Table,
File File
} }
const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end'];
function isKeyWord(word: string): boolean {
return keywords.includes(word);
}
export abstract class CodeBlock { export abstract class CodeBlock {
protected childBlocks: CodeBlock[] = []; protected childBlocks: CodeBlock[] = [];
private parentBlock?: CodeBlock; private parentBlock?: CodeBlock;
@@ -26,7 +32,7 @@ export abstract class CodeBlock {
public readonly sourceLineNumber?: number; public readonly sourceLineNumber?: number;
constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock){ constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock) {
this.sourceLineNumber = sourceLineNumber; this.sourceLineNumber = sourceLineNumber;
this.parentBlock = parentBlock; this.parentBlock = parentBlock;
this.blockType = blockType; this.blockType = blockType;
@@ -47,58 +53,57 @@ export abstract class CodeBlock {
if (!fs.existsSync(luaFilePath)) { if (!fs.existsSync(luaFilePath)) {
throw new Error(`File not found: ${luaFilePath}`); throw new Error(`File not found: ${luaFilePath}`);
} }
const fileContent = fs.readFileSync(luaFilePath, 'utf-8'); const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
let leftCursor = 0; let leftCursor = 0;
let lineCounter = 0; let lineCounter = 0;
const file : LuaFile = new LuaFile(); const file: LuaFile = new LuaFile();
let currentBlock : CodeBlock = file; let currentBlock: CodeBlock = file;
let currentWord = ''; let currentWord = '';
let currentBlockString = ''; let currentBlockString = '';
while(leftCursor < fileContent.length){ while (leftCursor < fileContent.length) {
const currentChar : string = fileContent[leftCursor]; const currentChar: string = fileContent[leftCursor];
currentWord += currentChar; currentWord += currentChar;
currentBlockString += currentChar; currentBlockString += currentChar;
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : ''; const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
if(currentChar === '\n'){ if (currentChar === '\n') {
lineCounter++; lineCounter++;
} }
if(currentChar === '-' && nextChar === '-'){ if (currentChar === '-' && nextChar === '-') {
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : ''; const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
if(nextNextChar === '['){ if (nextNextChar === '[') {
// Multiline comment, skip to closing ]] // Multiline comment, skip to closing ]]
leftCursor += 3; // Skip the --[ leftCursor += 3; // Skip the --[
while(leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')){ while (leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')) {
if(fileContent[leftCursor] === '\n'){ if (fileContent[leftCursor] === '\n') {
lineCounter++; lineCounter++;
} }
leftCursor++; leftCursor++;
} }
leftCursor += 2; // Skip the closing ]] leftCursor += 2; // Skip the closing ]]
} else { } else {
// Comment line, skip to end of line // Comment line, skip to end of line
while(leftCursor < fileContent.length && fileContent[leftCursor] !== '\n'){ while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
leftCursor++; leftCursor++;
} }
if(fileContent[leftCursor] === '\n'){ if (fileContent[leftCursor] === '\n') {
lineCounter++; lineCounter++;
} }
} }
currentWord = ''; currentWord = '';
continue; continue;
} }
else if (currentChar === '"' || currentChar === "'") {
if(currentChar === '"' || currentChar === "'"){
// String literal, skip to closing quote // String literal, skip to closing quote
const quoteType = currentChar; const quoteType = currentChar;
leftCursor++; leftCursor++;
while(leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType){ while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
// Add string but don't process it for keywords // Add string but don't process it for keywords
currentBlockString += fileContent[leftCursor]; currentBlockString += fileContent[leftCursor];
leftCursor++; leftCursor++;
@@ -109,61 +114,141 @@ export abstract class CodeBlock {
continue; continue;
} }
if(currentChar === "\n") { // In return block, read till the end of the return block
// else if (currentBlock.blockType === BlockType.Return) {
// //Continue reading until there's at least a new word
// }
else if (currentChar === "\n") {
// Process line // Process line
currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace
const lineBlock = new LineBlock(lineCounter, currentBlockString, currentBlock); currentBlockString += '\n'; // Add the newline back for the line block
if (currentBlockString !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
currentWord = '';
}
}
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace
currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock); currentBlock.childBlocks.push(lineBlock);
currentBlockString = ''; 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()!; currentBlock = currentBlock.getParentBlock()!;
} }
//Table blocks
else if (currentChar === "{") {
// Only create LineBlock if there's content before the brace
if (currentBlockString.trimEnd() !== '' && currentBlockString.trimEnd() !== '{') {
const currentLineBlock = new CodeTextBlock(lineCounter, currentBlockString.slice(0, -1).trimEnd(), currentBlock);
currentBlock.childBlocks.push(currentLineBlock);
}
if(nextChar.trim() === '' || nextChar === '('){ let leadingWhiteSpace = '';
const braceIndex = currentBlockString.lastIndexOf('{');
if (braceIndex > 0) {
let wsStart = braceIndex - 1;
while (wsStart >= 0 && /[ \t]/.test(currentBlockString[wsStart])) {
wsStart--;
}
leadingWhiteSpace = currentBlockString.substring(wsStart + 1, braceIndex);
}
currentBlockString = leadingWhiteSpace + '{'; // Start the new block string with the opening brace
const tableBlock = new TableBlock(lineCounter, currentBlock);
currentBlock.childBlocks.push(tableBlock);
currentBlock = tableBlock;
let braceCounter = 1;
leftCursor++;
while (leftCursor < fileContent.length && braceCounter > 0) {
const char = fileContent[leftCursor];
currentBlockString += char;
if (char === '{') {
braceCounter++;
} else if (char === '}') {
braceCounter--;
}
if (char === '\n') {
let block = currentBlockString.trimEnd();
block += '\n';
const lineBlock = new CodeTextBlock(lineCounter, block, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
lineCounter++;
}
leftCursor++;
}
// Find newline or other character
let tempCursor = leftCursor;
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
currentBlockString += fileContent[tempCursor];
tempCursor++;
}
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
currentBlockString += '\n';
lineCounter++;
} else {
leftCursor = tempCursor - 1; // Set leftCursor to the last character before the newline or non-whitespace character
}
if (currentBlockString !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
}
currentBlock = currentBlock.getParentBlock()!;
currentWord = ''; // Reset word accumulator after table block
continue; // Skip the leftCursor++ at the end of the loop since we already incremented it
}
else if (nextChar.trim() === '' || nextChar === '(') {
// End of a word, check for keywords // End of a word, check for keywords
const trimmedWord = currentWord.trim(); const trimmedWord = currentWord.trim();
let blockToAdd : CodeBlock | null = null; let blockToAdd: CodeBlock | null = null;
if(trimmedWord === 'if'){ if (trimmedWord === 'if') {
blockToAdd = new IfBlock(lineCounter, currentBlock); blockToAdd = new IfBlock(lineCounter, currentBlock);
} }
else if(trimmedWord === 'while'){ else if (trimmedWord === 'while') {
blockToAdd = new WhileBlock(lineCounter, currentBlock); blockToAdd = new WhileBlock(lineCounter, currentBlock);
} }
else if(trimmedWord === 'for'){ else if (trimmedWord === 'for') {
blockToAdd = new ForBlock(lineCounter, currentBlock); blockToAdd = new ForBlock(lineCounter, currentBlock);
} }
else if(trimmedWord === 'do'){ else if (trimmedWord === 'do') {
blockToAdd = new DoBlock(lineCounter, currentBlock); blockToAdd = new DoBlock(lineCounter, currentBlock);
} }
else if(trimmedWord === 'return'){ else if (trimmedWord === 'return') {
const line = new CodeTextBlock(lineCounter, currentBlockString.trimEnd().slice(0, -trimmedWord.length), currentBlock);
currentBlock.childBlocks.push(line);
currentBlockString = 'return';
blockToAdd = new ReturnBlock(lineCounter, currentBlock); blockToAdd = new ReturnBlock(lineCounter, currentBlock);
} }
else if(trimmedWord === 'function') { else if (trimmedWord === 'function') {
blockToAdd = new FunctionBlock(lineCounter, currentBlock); blockToAdd = new FunctionBlock(lineCounter, currentBlock);
} }
else if (trimmedWord === 'require') { else if (trimmedWord === 'require') {
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor); blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
} else if(trimmedWord === 'end'){ } else if (trimmedWord === 'end') {
const parent = currentBlock.getParentBlock(); const parent = currentBlock.getParentBlock();
if(!parent){ if (!parent) {
onError?.({ onError?.({
filePath: luaFilePath, filePath: luaFilePath,
line: fileContent.substring(0, leftCursor).split('\n').length - 1, line: fileContent.substring(0, leftCursor).split('\n').length - 1,
message: "Unexpected 'end' without matching block start" message: "Unexpected 'end' without matching block start"
}); });
} else {
currentBlock = parent;
} }
currentBlock = currentBlock.getParentBlock()! }
}
if(blockToAdd){ if (blockToAdd) {
currentBlock.childBlocks.push(blockToAdd); currentBlock.childBlocks.push(blockToAdd);
currentBlock = blockToAdd; currentBlock = blockToAdd;
} }
@@ -176,26 +261,26 @@ export abstract class CodeBlock {
} }
} }
export class LineBlock extends CodeBlock { export class CodeTextBlock extends CodeBlock {
constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock){ constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock) {
super(sourceLineNumber, BlockType.Line, parent); super(sourceLineNumber, BlockType.CodeTextBlock, parent);
} }
toLines(): string[] { toLines(): string[] {
return [this.line]; return [this.line];
} }
} }
export class IfBlock extends CodeBlock { export class IfBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.If, parent); super(sourceLineNumber, BlockType.If, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -204,13 +289,13 @@ export class IfBlock extends CodeBlock {
export class WhileBlock extends CodeBlock { export class WhileBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.While, parent); super(sourceLineNumber, BlockType.While, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -219,13 +304,13 @@ export class WhileBlock extends CodeBlock {
export class ForBlock extends CodeBlock { export class ForBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.For, parent); super(sourceLineNumber, BlockType.For, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -234,14 +319,14 @@ export class ForBlock extends CodeBlock {
export class LuaFile extends CodeBlock { export class LuaFile extends CodeBlock {
constructor() { constructor() {
super(0, BlockType.File); super(0, BlockType.File);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -250,13 +335,13 @@ export class LuaFile extends CodeBlock {
export class DoBlock extends CodeBlock { export class DoBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Do, parent); super(sourceLineNumber, BlockType.Do, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -265,13 +350,28 @@ export class DoBlock extends CodeBlock {
export class ReturnBlock extends CodeBlock { export class ReturnBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Return, parent); super(sourceLineNumber, BlockType.Return, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class TableBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Table, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -280,13 +380,13 @@ export class ReturnBlock extends CodeBlock {
export class FunctionBlock extends CodeBlock { export class FunctionBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock){ constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Function, parent); super(sourceLineNumber, BlockType.Function, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
@@ -295,25 +395,31 @@ export class FunctionBlock extends CodeBlock {
export class RequireBlock extends CodeBlock { export class RequireBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number){ constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number) {
super(sourceLineNumber, BlockType.Require, parent); super(sourceLineNumber, BlockType.Require, parent);
} }
toLines(): string[] { toLines(): string[] {
const lines: string[] = []; const lines: string[] = [];
for(const child of this.childBlocks){ for (const child of this.childBlocks) {
lines.push(...child.toLines()); lines.push(...child.toLines());
} }
return lines; return lines;
} }
getRequiredString(): string { getRequiredString(): string {
if(this.childBlocks.length === 0){ if (this.childBlocks.length === 0) {
return ''; return '';
} }
const firstChild = this.childBlocks[0]; const firstChild = this.childBlocks[0];
if(firstChild instanceof LineBlock){ if (firstChild instanceof CodeTextBlock) {
return firstChild.toLines()[0].trim().replace(/['"]/g, ''); // local module = require("module") -> module
// local module = require('module') -> module
const line = firstChild.toLines()[0];
const requireMatch = line.match(/require\s*\(\s*["']([^"']+)["']\s*\)/);
if (requireMatch && requireMatch[1]) {
return requireMatch[1];
}
} }
return ''; return '';
} }
+11 -7
View File
@@ -63,7 +63,9 @@ export class ScriptCompiler {
const fullPath = path.join(entry.parentPath, entry.name); const fullPath = path.join(entry.parentPath, entry.name);
const relativePath = path.relative(this.options.sourcePath, fullPath); const relativePath = path.relative(this.options.sourcePath, fullPath);
const luaFile = LuaFile.createFromFile(fullPath, this.options.onError); const luaFile = LuaFile.createFromFile(fullPath, this.options.onError);
const parsedFile = new ParsedFile(fileReferenceToLuaVariable(relativePath), luaFile, fullPath); const luaReference = fileReferenceToLuaVariable(relativePath);
const key = luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase();
const parsedFile = new ParsedFile(key, luaFile, fullPath);
parsedFiles.set(parsedFile.fileKey, parsedFile); parsedFiles.set(parsedFile.fileKey, parsedFile);
metricsMeter.filesRead++; metricsMeter.filesRead++;
@@ -97,6 +99,7 @@ export class ScriptCompiler {
class Dependency { class Dependency {
public readonly fileKey: string; public readonly fileKey: string;
public readonly luaReference: string;
constructor( constructor(
public readonly requiredModule: string, public readonly requiredModule: string,
@@ -105,7 +108,8 @@ class Dependency {
public readonly charEnd: number public readonly charEnd: number
) )
{ {
this.fileKey = fileReferenceToLuaVariable(requiredModule); this.luaReference = fileReferenceToLuaVariable(requiredModule);
this.fileKey = this.luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase();
} }
} }
@@ -182,8 +186,8 @@ class Writer {
private getStartLines(): string[] { private getStartLines(): string[] {
return [ return [
`-- Transpiled at (UTC): ${new Date().toISOString()}`, `-- Transpiled at (UTC): ${new Date().toISOString()}\n`,
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}` `local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}\n`
]; ];
} }
@@ -287,11 +291,11 @@ class Writer {
} }
} }
outputLines.push("do -- " + parsedFile.fileKey); outputLines.push("do -- " + parsedFile.fileKey + "\n");
const lines = parsedFile.luaFile.toLines(); const lines = parsedFile.luaFile.toLines();
metrics.totalLinesWritten += lines.length; metrics.totalLinesWritten += lines.length;
outputLines.push(...lines.filter(line => line.trim() !== '')); // Filter out empty lines outputLines.push(...lines.filter(line => line.trim() !== '')); // Filter out empty lines
outputLines.push("end -- " + parsedFile.fileKey); outputLines.push("end -- " + parsedFile.fileKey + "\n");
writtenFiles.add(parsedFile.fileKey); writtenFiles.add(parsedFile.fileKey);
metrics.filesWritten++; metrics.filesWritten++;
}; };
@@ -302,7 +306,7 @@ class Writer {
} }
fs.mkdirSync(path.dirname(this.location), { recursive: true }); fs.mkdirSync(path.dirname(this.location), { recursive: true });
fs.writeFileSync(this.location, outputLines.join('\n'), 'utf-8'); fs.writeFileSync(this.location, outputLines.join(''), 'utf-8');
if(includeDevScript) { if(includeDevScript) {
const devFileLocation = this.location.replace('.lua', '.dev.lua'); const devFileLocation = this.location.replace('.lua', '.dev.lua');