From fd55dad4ddd496e510a0614eeff30858feb34b9f Mon Sep 17 00:00:00 2001 From: dutchie031 <54616262+dutchie031@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:51:59 +0200 Subject: [PATCH 1/4] wip --- .vscode/extensions.json | 2 - .vscode/launch.json | 2 +- .vscode/tasks.json | 77 +---- compiler/src/CodeBlocks.ts | 320 ++++++++++++++++++ compiler/src/CompilationError.ts | 9 + compiler/src/ScriptCompiler.ts | 264 +++------------ dutchies-dcs-scripting-tools/esbuild.js | 1 + dutchies-dcs-scripting-tools/src/extension.ts | 1 + 8 files changed, 396 insertions(+), 280 deletions(-) create mode 100644 compiler/src/CodeBlocks.ts create mode 100644 compiler/src/CompilationError.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1243b10..ed11a21 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,8 +3,6 @@ // for the documentation about the extensions.json format "recommendations": [ "dbaeumer.vscode-eslint", - "connor4312.esbuild-problem-matchers", - "ms-vscode.extension-test-runner", "sumneko.lua" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index ef6e1f2..080dbe4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,7 @@ "${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js", "${workspaceFolder}/compiler/dist/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "compile" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 01673cc..8ed9136 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,70 +4,29 @@ "version": "2.0.0", "tasks": [ { - "label": "watch", - "dependsOn": [ - "npm: watch:tsc", - "npm: watch:esbuild" - ], - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - }, - "options": { - "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" - } - }, - { - "type": "npm", - "script": "watch:esbuild", - "group": "build", - "problemMatcher": "$esbuild-watch", - "isBackground": true, - "label": "npm: watch:esbuild", - "presentation": { - "group": "watch", - "reveal": "never" - }, - "options": { - "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" - } - }, - { - "type": "npm", - "script": "watch:tsc", - "group": "build", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "label": "npm: watch:tsc", - "presentation": { - "group": "watch", - "reveal": "never" - }, - "options": { - "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" - } - }, + "label": "compile", + "type": "npm", + "script": "compile", + "group": { + "kind": "build", + "isDefault": true + }, + "options": { + "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" + } + }, { + "label": "watch", "type": "npm", - "script": "watch-tests", - "problemMatcher": "$tsc-watch", + "script": "watch", "isBackground": true, "presentation": { - "reveal": "never", - "group": "watchers" + "reveal": "never" }, - "group": "build" - }, - { - "label": "tasks: watch-tests", - "dependsOn": [ - "npm: watch", - "npm: watch-tests" - ], - "problemMatcher": [] + "group": "build", + "options": { + "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" + } } ] } diff --git a/compiler/src/CodeBlocks.ts b/compiler/src/CodeBlocks.ts new file mode 100644 index 0000000..bb96c44 --- /dev/null +++ b/compiler/src/CodeBlocks.ts @@ -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 ''; + } +} \ No newline at end of file diff --git a/compiler/src/CompilationError.ts b/compiler/src/CompilationError.ts new file mode 100644 index 0000000..3a440af --- /dev/null +++ b/compiler/src/CompilationError.ts @@ -0,0 +1,9 @@ + + +export interface CompilationError { + filePath: string; + line: number; + charStart?: number; + charEnd?: number; + message: string; +} \ No newline at end of file diff --git a/compiler/src/ScriptCompiler.ts b/compiler/src/ScriptCompiler.ts index 9ff433d..ee12725 100644 --- a/compiler/src/ScriptCompiler.ts +++ b/compiler/src/ScriptCompiler.ts @@ -1,13 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; - -export interface CompilationError { - filePath: string; - line: number; - charStart?: number; - charEnd?: number; - message: string; -} +import { LuaFile, BlockType, CodeBlock, RequireBlock } from './CodeBlocks'; +import { CompilationError } from './CompilationError'; export interface ScriptCompilerOptions { sourcePath: string, @@ -23,6 +17,8 @@ export interface ICompilationLogger { writeLine(message: string): void; } +export { CompilationError }; + class Metrics { public totalLinesRead : number = 0; public totalLinesWritten: number = 0; @@ -66,9 +62,9 @@ export class ScriptCompiler { if (entry.isFile() && entry.name.endsWith('.lua')) { const fullPath = path.join(entry.parentPath, entry.name); const relativePath = path.relative(this.options.sourcePath, fullPath); - const content = fs.readFileSync(fullPath, 'utf-8'); - - const parsedFile = this.parseFile(relativePath, content, fullPath, metricsMeter); + const luaFile = LuaFile.createFromFile(fullPath, this.options.onError); + const parsedFile = new ParsedFile(fileReferenceToLuaVariable(relativePath), luaFile, fullPath); + parsedFiles.set(parsedFile.fileKey, parsedFile); metricsMeter.filesRead++; } @@ -83,6 +79,8 @@ export class ScriptCompiler { this.options.onError ); + writer.logDependencyTree(); + const writeStart = Date.now(); writer.write(includeDevScript, metricsMeter); const writeEnd = Date.now(); @@ -95,199 +93,6 @@ export class ScriptCompiler { metricsMeter.totalTimeMs = (end-start); metricsMeter.log(this.logger); } - - private reportError(filePath: string, line: number, charStart: number | undefined, charEnd: number | undefined, message: string): void { - if (this.options.onError) { - this.options.onError({ filePath, line, charStart, charEnd, message }); - } - } - - private parseFile(filePath: string, content: string, fullPath: string, metricsMeter: Metrics): ParsedFile { - const dependencies: Dependency[] = []; - const newLines: string[] = [`do --${filePath}`]; - - content = stripLuaMultilineComments(content); - const lines = content.split('\n').map(line => line.replace(/--.*$/, '')); - - const blockStack : string[] = []; - let isInFunction = false; - let foundModuleLevelReturn = false; - let expectingDo = false; - - const blockFound = (blockType: string): void => { - blockStack.push(blockType); - if (blockType === 'function') { - isInFunction = true; - } - }; - - const blockClosed = (): void => { - const closedBlock = blockStack.pop(); - if (closedBlock === 'function') { - isInFunction = blockStack.includes('function'); - } - }; - - for (let i = 0; i < lines.length; i++) { - metricsMeter.totalLinesRead++; - let line = lines[i]; - const trimmedLine = line.trim(); - - // Skip comments and blank lines - if (trimmedLine === '' || trimmedLine.startsWith('--')) { - continue; - } - - // If we found module-level return, only allow 'end' statements after it - if (foundModuleLevelReturn) { - // Check for 'end' keyword - if (/\bend\b/.test(trimmedLine)) { - const endMatches = trimmedLine.match(/\bend\b/g); - if (endMatches) { - for (let j = 0; j < endMatches.length; j++) { - blockClosed(); - } - } - newLines.push(line); - } else { - this.reportError(fullPath, i, undefined, undefined, `Code found after module-level return: ${trimmedLine}`); - newLines.push(line); // Continue processing despite error - } - continue; - } - - // Handle require statements - const requireMatch = line.match(/require\(['"](.+?)['"]\)/); - if (requireMatch) { - const textMatch = requireMatch[1]; - const requiredModule = fileReferenceToLuaVariable(textMatch); - - dependencies.push(new Dependency(textMatch, i, requireMatch.index ?? 0, (requireMatch.index ?? 0) + requireMatch[0].length)); - line = line.replace(requireMatch[0], requiredModule); - } - - // Track block keywords AND returns - need to process in order they appear - const keywords = [ - { regex: /\bfunction\b/, type: 'function' }, - { regex: /\bif\b/, type: 'if' }, - { regex: /\bfor\b/, type: 'for' }, - { regex: /\bwhile\b/, type: 'while' }, - { regex: /\bdo\b/, type: 'do' }, - { regex: /\bend\b/, type: 'end' }, - { regex: /\breturn\b/, type: 'return' } // Add return to the list! - ]; - - // Find positions of all keywords in the line - const foundKeywords: Array<{ position: number, type: string }> = []; - for (const kw of keywords) { - const matches = [...trimmedLine.matchAll(new RegExp(kw.regex, 'g'))]; - for (const match of matches) { - if (match.index !== undefined) { - foundKeywords.push({ position: match.index, type: kw.type }); - } - } - } - - // Sort by position to process in order - foundKeywords.sort((a, b) => a.position - b.position); - - // Process keywords in order - for (const kw of foundKeywords) { - if (kw.type === 'end') { - blockClosed(); - expectingDo = false; - } else if (kw.type === 'for' || kw.type === 'while') { - blockFound(kw.type); - expectingDo = true; - } else if (kw.type === 'do') { - if (!expectingDo) { - // Standalone do block - blockFound('do'); - } - expectingDo = false; - } else if (kw.type === 'return') { - // Handle return in sequence - if (!isInFunction && !foundModuleLevelReturn) { - // Extract the return value (everything after 'return') - const afterReturnPos = kw.position + 6; // 'return' is 6 chars - const afterReturn = trimmedLine.substring(afterReturnPos).trim(); - - if (afterReturn === '') { - this.reportError(fullPath, i, afterReturnPos, afterReturnPos, 'Empty return statement at module level'); - continue; - } - - // Check for multiple return values (commas outside of parentheses/braces/brackets) - let parenDepth = 0; - let braceDepth = 0; - let bracketDepth = 0; - let inString = false; - let stringChar = ''; - let hasMultipleValues = false; - - for (let j = 0; j < afterReturn.length; j++) { - const char = afterReturn[j]; - - if (!inString) { - if (char === '"' || char === "'") { - inString = true; - stringChar = char; - } else if (char === '(') { - parenDepth++; - } else if (char === ')') { - parenDepth--; - } else if (char === '{') { - braceDepth++; - } else if (char === '}') { - braceDepth--; - } else if (char === '[') { - bracketDepth++; - } else if (char === ']') { - bracketDepth--; - } else if (char === ',' && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) { - hasMultipleValues = true; - break; - } - } else { - if (char === stringChar && afterReturn[j - 1] !== '\\') { - inString = false; - } - } - } - - if (hasMultipleValues) { - this.reportError(fullPath, i, afterReturnPos, afterReturnPos + afterReturn.length, `Multiple return values not supported: ${trimmedLine}`); - } else { - // Replace return with assignment - const moduleVariable = fileReferenceToLuaVariable(filePath); - const parts = moduleVariable.split('.'); - for (let p = 1; p < parts.length; p++) { - const path = parts.slice(0, p + 1).join('.'); - newLines.push(`if not ${path} then ${path} = {} end`); - } - - line = line.replace(/\breturn\b/, moduleVariable + ' ='); - foundModuleLevelReturn = true; - } - } - } else { - // function or if - blockFound(kw.type); - expectingDo = false; - } - } - - newLines.push(line); - } - - newLines.push(`end --${filePath}`); - return new ParsedFile(filePath, fullPath, newLines, dependencies); - } -} - -function stripLuaMultilineComments(content: string): string { - // Matches --[[...]], --[=[...]=], --[==[...]==], etc. - return content.replace(/--\[(=*)\[[\s\S]*?\]\1\]/g, ''); } class Dependency { @@ -304,18 +109,6 @@ class Dependency { } } -class ParsedFile { - public readonly fileKey: string; - - constructor( - public readonly filePath: string, - public readonly fullPath: string, - public readonly lines: string[], - public readonly dependencies: Dependency[] - ) { - this.fileKey = fileReferenceToLuaVariable(filePath); - } -} function fileReferenceToLuaVariable(fileReference: string): string { // Remove .lua extension @@ -348,6 +141,37 @@ function fileReferenceToLuaVariable(fileReference: string): string { return result; } +class ParsedFile { + public readonly dependencies: Dependency[] = [] + + constructor( + public readonly fileKey: string, + public readonly luaFile: LuaFile, + public readonly fullPath: string + ){ + this.dependencies = ParsedFile.parseDependencies(luaFile); + } + + private static parseDependencies(luaFile: CodeBlock): Dependency[] { + // Recursively search for RequireBlocks in the LuaFile and its child blocks + const dependencies: Dependency[] = []; + const searchBlock = (block: CodeBlock) => { + if (block instanceof RequireBlock) { + const requiredString = block.getRequiredString(); + if (requiredString) { + dependencies.push(new Dependency(requiredString.toLowerCase(), block.sourceLineNumber ?? 0, block.charStart ?? 0, block.charEnd ?? 0)); + } + } + + for (const child of block.getChildren()) { + searchBlock(child); + } + } + searchBlock(luaFile); + return dependencies; + } +} + class Writer { constructor( public location: string, @@ -462,8 +286,12 @@ class Writer { }); } } - metrics.totalLinesWritten += parsedFile.lines.length; - outputLines.push(...parsedFile.lines); + + outputLines.push("do -- " + parsedFile.fileKey); + const lines = parsedFile.luaFile.toLines(); + metrics.totalLinesWritten += lines.length; + outputLines.push(...lines.filter(line => line.trim() !== '')); // Filter out empty lines + outputLines.push("end -- " + parsedFile.fileKey); writtenFiles.add(parsedFile.fileKey); metrics.filesWritten++; }; diff --git a/dutchies-dcs-scripting-tools/esbuild.js b/dutchies-dcs-scripting-tools/esbuild.js index a5117bb..ad31280 100644 --- a/dutchies-dcs-scripting-tools/esbuild.js +++ b/dutchies-dcs-scripting-tools/esbuild.js @@ -47,6 +47,7 @@ async function main() { }); if (watch) { await ctx.watch(); + console.log('[watch] watching for changes...'); } else { await ctx.rebuild(); await ctx.dispose(); diff --git a/dutchies-dcs-scripting-tools/src/extension.ts b/dutchies-dcs-scripting-tools/src/extension.ts index 6a9a872..4fcc20f 100644 --- a/dutchies-dcs-scripting-tools/src/extension.ts +++ b/dutchies-dcs-scripting-tools/src/extension.ts @@ -138,6 +138,7 @@ async function compileLuaScripts() { await compiler.compile(includeDevScript); } catch (err) { vscode.window.showErrorMessage('Compilation failed: ' + (err as Error).message); + logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack); } // Update diagnostics From 1dafe67cbdd99be734e25901d61aa516d8f770aa Mon Sep 17 00:00:00 2001 From: dutchie031 <54616262+dutchie031@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:26:01 +0200 Subject: [PATCH 2/4] wip --- .vscode/launch.json | 2 +- .vscode/tasks.json | 1 + compiler/src/CodeBlocks.ts | 242 ++++++++++++++++++++++++--------- compiler/src/ScriptCompiler.ts | 18 ++- 4 files changed, 187 insertions(+), 76 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 080dbe4..6c17764 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,7 @@ "${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js", "${workspaceFolder}/compiler/dist/**/*.js" ], - "preLaunchTask": "compile" + "preLaunchTask": "watch" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8ed9136..93f3e5a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -24,6 +24,7 @@ "reveal": "never" }, "group": "build", + "problemMatcher": "$tsc-watch", "options": { "cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools" } diff --git a/compiler/src/CodeBlocks.ts b/compiler/src/CodeBlocks.ts index bb96c44..fd74513 100644 --- a/compiler/src/CodeBlocks.ts +++ b/compiler/src/CodeBlocks.ts @@ -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. */ export enum BlockType { - Line, + CodeTextBlock, Function, If, While, @@ -16,9 +16,15 @@ export enum BlockType { Do, Return, Require, + Table, File } +const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end']; +function isKeyWord(word: string): boolean { + return keywords.includes(word); +} + export abstract class CodeBlock { protected childBlocks: CodeBlock[] = []; private parentBlock?: CodeBlock; @@ -26,7 +32,7 @@ export abstract class CodeBlock { public readonly sourceLineNumber?: number; - constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock){ + constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock) { this.sourceLineNumber = sourceLineNumber; this.parentBlock = parentBlock; this.blockType = blockType; @@ -47,58 +53,57 @@ export abstract class CodeBlock { 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; - + const file: LuaFile = new LuaFile(); + let currentBlock: CodeBlock = file; + let currentWord = ''; let currentBlockString = ''; - - while(leftCursor < fileContent.length){ - const currentChar : string = fileContent[leftCursor]; + + 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'){ + if (currentChar === '\n') { lineCounter++; } - if(currentChar === '-' && nextChar === '-'){ + 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 === '['){ + if (nextNextChar === '[') { // Multiline comment, skip to closing ]] leftCursor += 3; // Skip the --[ - while(leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')){ - if(fileContent[leftCursor] === '\n'){ + 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'){ + // Comment line, skip to end of line + while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') { leftCursor++; } - if(fileContent[leftCursor] === '\n'){ + if (fileContent[leftCursor] === '\n') { lineCounter++; } } currentWord = ''; continue; } - - if(currentChar === '"' || currentChar === "'"){ + else if (currentChar === '"' || currentChar === "'") { // String literal, skip to closing quote const quoteType = currentChar; leftCursor++; - while(leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType){ + while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) { // Add string but don't process it for keywords currentBlockString += fileContent[leftCursor]; leftCursor++; @@ -109,61 +114,141 @@ export abstract class CodeBlock { 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 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); 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()!; - } + 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(currentChar === ")" && currentBlock.blockType === BlockType.Require){ - currentBlock = currentBlock.getParentBlock()!; - } + 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(nextChar.trim() === '' || nextChar === '('){ + 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 const trimmedWord = currentWord.trim(); - let blockToAdd : CodeBlock | null = null; - if(trimmedWord === 'if'){ + let blockToAdd: CodeBlock | null = null; + if (trimmedWord === 'if') { blockToAdd = new IfBlock(lineCounter, currentBlock); } - else if(trimmedWord === 'while'){ + else if (trimmedWord === 'while') { blockToAdd = new WhileBlock(lineCounter, currentBlock); } - else if(trimmedWord === 'for'){ + else if (trimmedWord === 'for') { blockToAdd = new ForBlock(lineCounter, currentBlock); } - else if(trimmedWord === 'do'){ + else if (trimmedWord === 'do') { 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); } - else if(trimmedWord === 'function') { + 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'){ + } else if (trimmedWord === 'end') { const parent = currentBlock.getParentBlock(); - if(!parent){ + if (!parent) { onError?.({ filePath: luaFilePath, line: fileContent.substring(0, leftCursor).split('\n').length - 1, message: "Unexpected 'end' without matching block start" }); + } else { + currentBlock = parent; } - currentBlock = currentBlock.getParentBlock()! - } + } - if(blockToAdd){ + if (blockToAdd) { currentBlock.childBlocks.push(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){ - super(sourceLineNumber, BlockType.Line, parent); + constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock) { + super(sourceLineNumber, BlockType.CodeTextBlock, parent); } - + toLines(): string[] { return [this.line]; - } + } } export class IfBlock extends CodeBlock { - - constructor(sourceLineNumber: number, parent: CodeBlock){ + + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.If, parent); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -204,13 +289,13 @@ export class IfBlock extends CodeBlock { export class WhileBlock extends CodeBlock { - constructor(sourceLineNumber: number, parent: CodeBlock){ + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.While, parent); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -219,13 +304,13 @@ export class WhileBlock extends CodeBlock { export class ForBlock extends CodeBlock { - constructor(sourceLineNumber: number, parent: CodeBlock){ + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.For, parent); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -234,14 +319,14 @@ export class ForBlock extends CodeBlock { export class LuaFile extends CodeBlock { - + constructor() { super(0, BlockType.File); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -250,13 +335,13 @@ export class LuaFile extends CodeBlock { export class DoBlock extends CodeBlock { - constructor(sourceLineNumber: number, parent: CodeBlock){ + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.Do, parent); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -265,13 +350,28 @@ export class DoBlock extends CodeBlock { export class ReturnBlock extends CodeBlock { - constructor(sourceLineNumber: number, parent: CodeBlock){ + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.Return, parent); } toLines(): 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()); } return lines; @@ -280,13 +380,13 @@ export class ReturnBlock extends CodeBlock { export class FunctionBlock extends CodeBlock { - constructor(sourceLineNumber: number, parent: CodeBlock){ + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.Function, parent); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; @@ -295,25 +395,31 @@ export class FunctionBlock 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); } toLines(): string[] { const lines: string[] = []; - for(const child of this.childBlocks){ + for (const child of this.childBlocks) { lines.push(...child.toLines()); } return lines; } getRequiredString(): string { - if(this.childBlocks.length === 0){ + if (this.childBlocks.length === 0) { return ''; } const firstChild = this.childBlocks[0]; - if(firstChild instanceof LineBlock){ - return firstChild.toLines()[0].trim().replace(/['"]/g, ''); + if (firstChild instanceof CodeTextBlock) { + // 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 ''; } diff --git a/compiler/src/ScriptCompiler.ts b/compiler/src/ScriptCompiler.ts index ee12725..3ef527d 100644 --- a/compiler/src/ScriptCompiler.ts +++ b/compiler/src/ScriptCompiler.ts @@ -63,7 +63,9 @@ export class ScriptCompiler { const fullPath = path.join(entry.parentPath, entry.name); const relativePath = path.relative(this.options.sourcePath, fullPath); 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); metricsMeter.filesRead++; @@ -97,6 +99,7 @@ export class ScriptCompiler { class Dependency { public readonly fileKey: string; + public readonly luaReference: string; constructor( public readonly requiredModule: string, @@ -105,7 +108,8 @@ class Dependency { 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[] { return [ - `-- Transpiled at (UTC): ${new Date().toISOString()}`, - `local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}` + `-- Transpiled at (UTC): ${new Date().toISOString()}\n`, + `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(); metrics.totalLinesWritten += lines.length; 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); metrics.filesWritten++; }; @@ -302,7 +306,7 @@ class Writer { } 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) { const devFileLocation = this.location.replace('.lua', '.dev.lua'); From bc6730ff53eaf3474d9ca4cfea8436bc89ece5bb Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Wed, 8 Jul 2026 15:48:16 +0200 Subject: [PATCH 3/4] Fixed keywords and end blocks --- compiler/src/CodeBlocks.ts | 191 +++++++++++++++++++++++++++++---- compiler/src/ScriptCompiler.ts | 100 +++++++++++++++-- 2 files changed, 266 insertions(+), 25 deletions(-) diff --git a/compiler/src/CodeBlocks.ts b/compiler/src/CodeBlocks.ts index fd74513..a3ca17e 100644 --- a/compiler/src/CodeBlocks.ts +++ b/compiler/src/CodeBlocks.ts @@ -20,11 +20,22 @@ export enum BlockType { File } -const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end']; +const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end', 'local', 'else', 'elseif']; function isKeyWord(word: string): boolean { return keywords.includes(word); } +function isIdentifierCharacter(char: string): boolean { + return /[A-Za-z0-9_]/.test(char); +} + +/** + * Trims trailing spaces and tabs, but preserves newlines + */ +function trimEndPreserveNewlines(str: string): string { + return str.replace(/[ \t]+$/gm, ''); +} + export abstract class CodeBlock { protected childBlocks: CodeBlock[] = []; private parentBlock?: CodeBlock; @@ -66,7 +77,11 @@ export abstract class CodeBlock { while (leftCursor < fileContent.length) { const currentChar: string = fileContent[leftCursor]; - currentWord += currentChar; + if (isIdentifierCharacter(currentChar)) { + currentWord += currentChar; + } else { + currentWord = ''; + } currentBlockString += currentChar; const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : ''; @@ -121,8 +136,8 @@ export abstract class CodeBlock { // } else if (currentChar === "\n") { // Process line - currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace - currentBlockString += '\n'; // Add the newline back for the line block + currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs + //currentBlockString += '\n'; // Add the newline back for the line block if (currentBlockString !== '') { const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock); currentBlock.childBlocks.push(lineBlock); @@ -131,8 +146,8 @@ export abstract class CodeBlock { } } 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 + currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs + //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); currentBlockString = ''; @@ -142,8 +157,8 @@ export abstract class CodeBlock { //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); + if (trimEndPreserveNewlines(currentBlockString) !== '' && trimEndPreserveNewlines(currentBlockString) !== '{') { + const currentLineBlock = new CodeTextBlock(lineCounter, trimEndPreserveNewlines(currentBlockString.slice(0, -1)), currentBlock); currentBlock.childBlocks.push(currentLineBlock); } @@ -174,8 +189,7 @@ export abstract class CodeBlock { } if (char === '\n') { - let block = currentBlockString.trimEnd(); - block += '\n'; + let block = trimEndPreserveNewlines(currentBlockString); const lineBlock = new CodeTextBlock(lineCounter, block, currentBlock); currentBlock.childBlocks.push(lineBlock); currentBlockString = ''; @@ -194,11 +208,13 @@ export abstract class CodeBlock { 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 + } else if (tempCursor > leftCursor) { + // We found whitespace but no newline, so position cursor at last whitespace + leftCursor = tempCursor - 1; } + // else: no whitespace after table, leave leftCursor where it is - if (currentBlockString !== '') { + if (trimEndPreserveNewlines(currentBlockString) !== '') { const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock); currentBlock.childBlocks.push(lineBlock); currentBlockString = ''; @@ -208,10 +224,22 @@ export abstract class CodeBlock { 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 === '(') { + else if (currentWord !== '' && !isIdentifierCharacter(nextChar)) { // End of a word, check for keywords const trimmedWord = currentWord.trim(); + + + if (currentBlock.blockType === BlockType.Return && isKeyWord(trimmedWord) && trimmedWord !== 'return') { + // We're in a ReturnBlock and hit a keyword at the statement boundary + // Extract return params and exit the block + (currentBlock as ReturnBlock).extractReturnParams(); + currentBlock = currentBlock.getParentBlock()!; + // Don't clear currentBlockString - let normal flow handle the newline finalization + // Continue to process this keyword normally + } + let blockToAdd: CodeBlock | null = null; + if (trimmedWord === 'if') { blockToAdd = new IfBlock(lineCounter, currentBlock); } @@ -222,20 +250,39 @@ export abstract class CodeBlock { blockToAdd = new ForBlock(lineCounter, currentBlock); } else if (trimmedWord === 'do') { - blockToAdd = new DoBlock(lineCounter, currentBlock); + const isForOrWhile = currentBlock.blockType === BlockType.While || currentBlock.blockType === BlockType.For; + if(isForOrWhile && (currentBlock as WhileBlock | ForBlock).passedDoStatement == false) { + // If we're in a While or For block and we've not yet passed a 'do' statement, mark it as passed and don't create a new block + (currentBlock as WhileBlock | ForBlock).passedDoStatement = true; + } else { + blockToAdd = new DoBlock(lineCounter, currentBlock); + } } else if (trimmedWord === 'return') { - const line = new CodeTextBlock(lineCounter, currentBlockString.trimEnd().slice(0, -trimmedWord.length), currentBlock); - currentBlock.childBlocks.push(line); - currentBlockString = 'return'; + // Add any text before 'return' to parent block, but preserve newlines + const beforeReturn = currentBlockString.slice(0, -trimmedWord.length); + if (beforeReturn.trim()) { + const line = new CodeTextBlock(lineCounter, beforeReturn, currentBlock); + currentBlock.childBlocks.push(line); + } blockToAdd = new ReturnBlock(lineCounter, currentBlock); + // Start the return block content with 'return' keyword + currentBlockString = trimmedWord; } else if (trimmedWord === 'function') { blockToAdd = new FunctionBlock(lineCounter, currentBlock); } else if (trimmedWord === 'require') { + const beforeRequire = currentBlockString.slice(0, -trimmedWord.length); + if (beforeRequire.trim()) { + const line = new CodeTextBlock(lineCounter, beforeRequire, currentBlock); + currentBlock.childBlocks.push(line); + } + blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor); - } else if (trimmedWord === 'end') { + currentBlockString = trimmedWord; // Start the require block content with 'require' keyword + } + else if (trimmedWord === 'end') { const parent = currentBlock.getParentBlock(); if (!parent) { onError?.({ @@ -257,6 +304,14 @@ export abstract class CodeBlock { } leftCursor++; } + // Handle case where file ends while in a ReturnBlock + if (currentBlock.blockType === BlockType.Return) { + if (trimEndPreserveNewlines(currentBlockString) !== '') { + const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock); + currentBlock.childBlocks.push(lineBlock); + } + (currentBlock as ReturnBlock).extractReturnParams(); + } return file; } } @@ -289,6 +344,8 @@ export class IfBlock extends CodeBlock { export class WhileBlock extends CodeBlock { + public passedDoStatement: boolean = false; + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.While, parent); } @@ -304,6 +361,8 @@ export class WhileBlock extends CodeBlock { export class ForBlock extends CodeBlock { + public passedDoStatement: boolean = false; + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.For, parent); } @@ -350,6 +409,8 @@ export class DoBlock extends CodeBlock { export class ReturnBlock extends CodeBlock { + public readonly returnParams: string[] = []; + constructor(sourceLineNumber: number, parent: CodeBlock) { super(sourceLineNumber, BlockType.Return, parent); } @@ -361,6 +422,98 @@ export class ReturnBlock extends CodeBlock { } return lines; } + + /** + * Extracts return parameters from the return statement. + * Splits by commas while respecting nesting of {} and (). + * Populates the returnParams array. + */ + extractReturnParams(): void { + // Reconstruct the return content from all children + let content = this.childBlocks + .map(child => { + if (child instanceof CodeTextBlock) { + return child.toLines()[0]; + } else if (child instanceof TableBlock) { + // For tables, use their full content + return child.toLines().join(''); + } else { + // For other block types, use their full content + return child.toLines().join(''); + } + }) + .join('') + .trim(); + + if (!content) { + this.returnParams.length = 0; + return; + } + + content = content.replace(/^return\s+/, '').trim(); // Remove the 'return' keyword if present + + // Split by commas while respecting nesting + const params: string[] = []; + let currentParam = ''; + let braceDepth = 0; + let parenDepth = 0; + + for (let i = 0; i < content.length; i++) { + const char = content[i]; + const nextChar = i < content.length - 1 ? content[i + 1] : ''; + + // Skip strings to avoid counting delimiters inside them + if (char === '"' || char === "'") { + const quoteType = char; + currentParam += char; + i++; + while (i < content.length && content[i] !== quoteType) { + if (content[i] === '\\' && i + 1 < content.length) { + currentParam += content[i]; + i++; + currentParam += content[i]; + } else { + currentParam += content[i]; + } + i++; + } + if (i < content.length) { + currentParam += content[i]; + } + continue; + } + + // Track nesting depth + if (char === '{') { + braceDepth++; + } else if (char === '}') { + braceDepth--; + } else if (char === '(') { + parenDepth++; + } else if (char === ')') { + parenDepth--; + } else if (char === ',' && braceDepth === 0 && parenDepth === 0) { + // This is a param separator + const param = currentParam.trim(); + if (param) { + params.push(param); + } + currentParam = ''; + continue; + } + + currentParam += char; + } + + // Add the last parameter + const lastParam = currentParam.trim(); + if (lastParam) { + params.push(lastParam); + } + + this.returnParams.length = 0; + this.returnParams.push(...params); + } } export class TableBlock extends CodeBlock { diff --git a/compiler/src/ScriptCompiler.ts b/compiler/src/ScriptCompiler.ts index 3ef527d..bbb4593 100644 --- a/compiler/src/ScriptCompiler.ts +++ b/compiler/src/ScriptCompiler.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { LuaFile, BlockType, CodeBlock, RequireBlock } from './CodeBlocks'; +import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks'; import { CompilationError } from './CompilationError'; export interface ScriptCompilerOptions { @@ -130,7 +130,7 @@ function fileReferenceToLuaVariable(fileReference: string): string { // Split by / to get path parts const parts = fileReference.split('/'); - + // Convert to ScriptGlobals.folder.FileName format let result = LUA_SCRIPT_GLOBAL_KEYWORD; for (let i = 0; i < parts.length; i++) { @@ -174,6 +174,64 @@ class ParsedFile { searchBlock(luaFile); return dependencies; } + + public replaceRequireWithGlobal(): void { + + function replaceRequireInBlock(block: CodeBlock) { + + if (block instanceof RequireBlock) { + const requiredString = block.getRequiredString(); + if (requiredString) { + const luaReference = fileReferenceToLuaVariable(requiredString); + block.toLines = () => [`${luaReference}\n`]; + } + } + + for (const child of block.getChildren()) { + replaceRequireInBlock(child); + } + } + + for (const child of this.luaFile.getChildren()) { + replaceRequireInBlock(child); + } + + } + + public replaceModuleReturnWithGlobalAssignement(): void { + + const replaceReturnInBlock = (block: CodeBlock) => { + if (block instanceof ReturnBlock) { + const parent = block.getParentBlock(); + if (parent) { + const luaReference = fileReferenceToLuaVariable(this.fileKey); + const resultLines : string[] = []; + + const splitCount = luaReference.split('.').length; + for(let i = 2; i <= splitCount -1 ; i++){ + const partialReference = luaReference.split('.').slice(0, i).join('.'); + resultLines.push(`if not ${partialReference} then ${partialReference} = {} end`); + } + + resultLines.push(`${luaReference} = ${block.returnParams.join(', ')}`); + block.toLines = () => resultLines.map(line => line + '\n'); + } + } + + if(block instanceof FunctionBlock || block instanceof TableBlock){ + return; // Do not traverse into FunctionBlock or TableBlock + } + + for (const child of block.getChildren()) { + replaceReturnInBlock(child); + } + } + + for (const child of this.luaFile.getChildren()) { + replaceReturnInBlock(child); + } + + } } class Writer { @@ -291,11 +349,41 @@ class Writer { } } - outputLines.push("do -- " + parsedFile.fileKey + "\n"); + parsedFile.replaceRequireWithGlobal(); + parsedFile.replaceModuleReturnWithGlobalAssignement(); + const lines = parsedFile.luaFile.toLines(); - metrics.totalLinesWritten += lines.length; - outputLines.push(...lines.filter(line => line.trim() !== '')); // Filter out empty lines - outputLines.push("end -- " + parsedFile.fileKey + "\n"); + const newLineFilteredLines : string[] = []; + + function trimEndPreserveNewlines(str: string): string { + return str.replace(/[ \t]+$/gm, ''); + } + + + let wasLastEmpty = false; + let lastEndedWithNewline = false; + for(const line of lines){ + if(trimEndPreserveNewlines(line) !== ''){ + if(line.trim() === ''){ + if(!wasLastEmpty && !lastEndedWithNewline){ + newLineFilteredLines.push(line); + wasLastEmpty = true; + } + } else { + newLineFilteredLines.push(line); + wasLastEmpty = false; + + lastEndedWithNewline = line.endsWith('\n'); + } + + } + } + + outputLines.push("do -- " + parsedFile.fileKey + "\n"); + + metrics.totalLinesWritten += newLineFilteredLines.length; + outputLines.push(...newLineFilteredLines); + outputLines.push("\nend -- " + parsedFile.fileKey + "\n"); writtenFiles.add(parsedFile.fileKey); metrics.filesWritten++; }; From 3cd8782a8eed55798c08fcae63815c2e95c1b88f Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Tue, 14 Jul 2026 20:33:04 +0200 Subject: [PATCH 4/4] fixed version tested and runs in DCS now --- compiler/src/ScriptCompiler.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/compiler/src/ScriptCompiler.ts b/compiler/src/ScriptCompiler.ts index bbb4593..556f954 100644 --- a/compiler/src/ScriptCompiler.ts +++ b/compiler/src/ScriptCompiler.ts @@ -65,6 +65,16 @@ export class ScriptCompiler { const luaFile = LuaFile.createFromFile(fullPath, this.options.onError); const luaReference = fileReferenceToLuaVariable(relativePath); const key = luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase(); + + if(parsedFiles.has(key)){ + this.options.onError?.({ + filePath: fullPath, + line: 0, + message: `Duplicate file key detected: ${key}. This can happen if two files have different capitalization. Lua is case sensitive, but the compiler treats file keys as case insensitive.` + }); + continue; + } + const parsedFile = new ParsedFile(key, luaFile, fullPath); parsedFiles.set(parsedFile.fileKey, parsedFile); @@ -134,13 +144,7 @@ function fileReferenceToLuaVariable(fileReference: string): string { // Convert to ScriptGlobals.folder.FileName format let result = LUA_SCRIPT_GLOBAL_KEYWORD; for (let i = 0; i < parts.length; i++) { - if (i === parts.length - 1) { - // Capitalize first letter of filename - result += '.' + parts[i].charAt(0).toUpperCase() + parts[i].slice(1); - } else { - // Folder names stay lowercase - result += '.' + parts[i]; - } + result += '.' + parts[i].toLowerCase(); } return result; }