luma-lang
Version:
The Embeddable Luma Language Compiler and Runtime
1 lines • 340 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/LumaError.ts","../src/Tokenizer/Keywords.ts","../src/Tokenizer/Operators.ts","../src/Tokenizer/Punctuation.ts","../src/Tokenizer/TokenStream.ts","../src/Tokenizer/TokenType.ts","../src/Tokenizer/Tokenizer.ts","../src/Parser/Parser.ts","../src/Program/Opcodes.ts","../src/Program/Binary/BinaryReader.ts","../src/Program/ConstantPool.ts","../src/Program/Reader.ts","../src/Program/Binary/BinaryWriter.ts","../src/Program/Writer.ts","../src/Compiler/Compiler.ts","../src/LumaRuntimeError.ts","../src/Utility/ANSI.ts","../src/VM/Deserializer.ts","../src/VM/ForbiddenKeys.ts","../src/VM/NativeDispatcher.ts","../src/VM/InstructionSet.ts","../src/VM/Serializer.ts","../src/VM/State.ts","../src/VM/VirtualMachine.ts","../src/Debugger/Debugger.ts"],"sourcesContent":["export * from './core.js';\nexport * from './Debugger/index.js';\n","import { TokenPosition } from './Tokenizer/index.js';\n\nexport class LumaError extends Error\n{\n public readonly moduleName?: string;\n public readonly position?: TokenPosition;\n\n constructor(options: LumaErrorOptions)\n {\n super(options.message, {\n cause: options.cause,\n });\n\n this.moduleName = options.moduleName;\n this.position = options.position;\n }\n}\n\nexport type LumaErrorOptions = {\n message: string;\n moduleName?: string;\n position?: TokenPosition;\n cause?: Error,\n}\n","export const Keywords = [\n 'fn',\n 'on',\n 'if',\n 'else',\n 'do',\n 'while',\n 'for',\n 'in',\n 'break',\n 'continue',\n 'return',\n 'true',\n 'false',\n 'null',\n 'not',\n 'this',\n 'and',\n 'or',\n 'local',\n 'public',\n 'import',\n 'wait',\n 'new',\n 'class',\n 'extends',\n 'parent',\n] as const;\n","export const Operators = {\n ASSIGN: '=',\n PLUS: '+',\n MINUS: '-',\n MULTIPLY: '*',\n DIVIDE: '/',\n MODULO: '%',\n EXPONENTIATION: '^',\n EQUALS: '==',\n NOT_EQUALS: '!=',\n GREATER_THAN: '>',\n LESS_THAN: '<',\n GREATER_EQUAL: '>=',\n LESS_EQUAL: '<=',\n NOT: '!',\n RANGE: '..',\n IS: 'is',\n} as const;\n","export const Punctuation = {\n MEMBER_ACCESS: '.',\n COMMA: ',',\n COLON: ':',\n QUESTION_MARK: '?',\n OPEN_PAREN: '(',\n CLOSE_PAREN: ')',\n OPEN_BRACKET: '[',\n CLOSE_BRACKET: ']',\n OPEN_BRACE: '{',\n CLOSE_BRACE: '}',\n BANG: '!',\n} as const;\n","import { Token } from './Token.js';\n\nexport class TokenStream\n{\n private readonly _tokens: Token[];\n private _index: number = 0;\n\n constructor(tokens: Token[])\n {\n this._tokens = tokens;\n }\n\n public get tokens(): Token[]\n {\n return [...this._tokens];\n }\n\n public get length(): number\n {\n return this._tokens.length;\n }\n\n public peek(offset: number = 0): Token | null\n {\n const targetIndex = this._index + offset;\n\n if (targetIndex < 0 || targetIndex >= this._tokens.length) {\n return null;\n }\n\n return this._tokens[targetIndex];\n }\n\n public consume(): Token\n {\n const token = this.peek(0);\n if (! token) {\n throw new Error('Unexpected End of File');\n }\n this._index++;\n return token;\n }\n\n public get isEof(): boolean\n {\n return this._index >= this._tokens.length;\n }\n}\n","export const TokenType = {\n IDENTIFIER: 'IDENTIFIER',\n KEYWORD: 'KEYWORD',\n OPERATOR: 'OPERATOR',\n PUNCTUATION: 'PUNCTUATION',\n STRING: 'STRING',\n NUMBER: 'NUMBER',\n COMMENT: 'COMMENT',\n BLOCK_COMMENT: 'BLOCK_COMMENT',\n NEWLINE: 'NEWLINE',\n INDENT: 'INDENT',\n DEDENT: 'DEDENT',\n EOF: 'EOF',\n} as const;\n","import { LumaError } from '../LumaError.js';\nimport { Keywords } from './Keywords.js';\nimport { Operators } from './Operators.js';\nimport { Punctuation } from './Punctuation.js';\nimport { Token } from './Token.js';\nimport { TokenStream } from './TokenStream.js';\nimport { TokenType } from './TokenType.js';\n\nexport class Tokenizer\n{\n private static readonly KEYWORD_SET = new Set<string>(Keywords);\n private static readonly PUNCTUATION_SET = new Set<string>(Object.values(Punctuation));\n private static readonly SYMBOL_MAP = new Map<string, string>([\n ...Object.entries(Operators).map(([k, v]) => [v, k] as [string, string]),\n ...Object.entries(Punctuation).map(([k, v]) => [v, k] as [string, string]),\n ]);\n private static readonly SORTED_SYMBOLS = Array\n .from(Tokenizer.SYMBOL_MAP.keys())\n .sort((a, b) => b.length - a.length);\n\n private readonly _tokens: Token[] = [];\n private readonly _source: string;\n private readonly _moduleName: string | undefined;\n\n private index: number = 0;\n private line: number = 1;\n private col: number = 1;\n private indentStack: number[] = [0];\n private isAtStartOfLine: boolean = true;\n\n private constructor(source: string, moduleName: string | undefined)\n {\n this._source = source.replace(/\\r\\n/g, '\\n');\n this._moduleName = moduleName;\n }\n\n public static tokenize(source: string, moduleName: string | undefined = undefined): TokenStream\n {\n if (! source.endsWith('\\n')) {\n source += '\\n';\n }\n\n return new Tokenizer(source, moduleName).tokenize();\n }\n\n private tokenize(): TokenStream\n {\n while (! this.isEof) {\n if (this.isAtStartOfLine) {\n if (this.handleIndentation()) {\n continue;\n }\n }\n\n const char = this._source[this.index];\n\n if (char === '\\n') {\n this.handleNewline();\n continue;\n }\n\n if (char === ' ' || char === '\\t') {\n this.advance(1);\n continue;\n }\n\n if (this.parseComment()) continue;\n if (this.parseBlockComment()) continue;\n if (this.parseSymbol()) continue;\n if (this.parseNumberLiteral()) continue;\n if (this.parseStringLiteral()) continue;\n if (this.parseWord()) continue;\n\n this.throwUnexpectedCharacterError();\n }\n\n while (this.indentStack.length > 1) {\n this.indentStack.pop();\n this._tokens.push(this.createToken(TokenType.DEDENT, ''));\n }\n\n return new TokenStream(this._tokens);\n }\n\n private handleNewline()\n {\n const lastToken = this._tokens[this._tokens.length - 1];\n if (lastToken && lastToken.type !== TokenType.NEWLINE && lastToken.type !== TokenType.INDENT && lastToken.type !== TokenType.DEDENT) {\n this._tokens.push(this.createToken(TokenType.NEWLINE, '\\\\n'));\n }\n\n this.index++;\n this.line++;\n this.col = 1;\n this.isAtStartOfLine = true;\n }\n\n private handleIndentation(): boolean\n {\n let spaces = 0;\n let tempIndex = this.index;\n\n while (tempIndex < this._source.length && (this._source[tempIndex] === ' ' || this._source[tempIndex] === '\\t')) {\n spaces += this._source[tempIndex] === '\\t' ? 4 : 1;\n tempIndex++;\n }\n\n const char = this._source[tempIndex];\n\n if (tempIndex >= this._source.length || char === '\\n' || this._source.startsWith('//', tempIndex) || this._source.startsWith('/*', tempIndex)) {\n this.isAtStartOfLine = false;\n return false;\n }\n\n this.advance(tempIndex - this.index);\n\n const currentIndent = this.indentStack[this.indentStack.length - 1];\n\n if (spaces > currentIndent) {\n this.indentStack.push(spaces);\n this._tokens.push(this.createToken(TokenType.INDENT, spaces.toString()));\n } else if (spaces < currentIndent) {\n while (spaces < this.indentStack[this.indentStack.length - 1]) {\n this.indentStack.pop();\n this._tokens.push(this.createToken(TokenType.DEDENT, ''));\n }\n if (spaces !== this.indentStack[this.indentStack.length - 1]) {\n this.throwError('Indentation error: Indent level does not match any outer block');\n }\n }\n\n this.isAtStartOfLine = false;\n return false;\n }\n\n private parseWord(): boolean\n {\n let tempIndex = this.index;\n if (! /[a-zA-Z_]/.test(this._source[tempIndex])) return false;\n\n while (tempIndex < this._source.length && /[a-zA-Z0-9_]/.test(this._source[tempIndex])) {\n tempIndex++;\n }\n\n const value = this._source.slice(this.index, tempIndex);\n const type = Tokenizer.KEYWORD_SET.has(value) ? TokenType.KEYWORD : TokenType.IDENTIFIER;\n\n this._tokens.push(this.createToken(type, value));\n this.advance(value.length);\n return true;\n }\n\n private parseSymbol(): boolean\n {\n for (const symbol of Tokenizer.SORTED_SYMBOLS) {\n if (this._source.startsWith(symbol, this.index)) {\n const type = Tokenizer.PUNCTUATION_SET.has(symbol)\n ? TokenType.PUNCTUATION\n : TokenType.OPERATOR;\n\n this._tokens.push(this.createToken(type, symbol));\n this.advance(symbol.length);\n return true;\n }\n }\n return false;\n }\n\n private parseNumberLiteral(): boolean\n {\n const char = this._source[this.index];\n const nextChar = this._source[this.index + 1];\n\n // Check start: Digit OR Dot followed by Digit (e.g. .5)\n // explicitly NOT matching \"..\" (Range) here because nextChar must be a digit\n const isDigit = char >= '0' && char <= '9';\n const isDotStart = char === '.' && (nextChar >= '0' && nextChar <= '9');\n\n if (! isDigit && ! isDotStart) return false;\n\n let tempIndex = this.index;\n let hasDot = false; // Track if we've consumed a decimal point\n\n // Manual loop instead of Regex to control the dot logic\n while (tempIndex < this._source.length) {\n const c = this._source[tempIndex];\n const next = this._source[tempIndex + 1];\n\n if (c >= '0' && c <= '9') {\n tempIndex++;\n continue;\n }\n\n if (c === '_') {\n tempIndex++;\n continue;\n }\n\n if (c === '.') {\n // CRITICAL FIX: If we see a dot, check if the NEXT char is also a dot.\n // If it is, this is a Range operator (..), so the number stops HERE.\n if (next === '.') {\n break;\n }\n\n // Standard float logic: Only one dot allowed per number\n if (hasDot) {\n break; // We found a second dot (e.g. 1.2.3), stop parsing.\n }\n\n hasDot = true;\n tempIndex++;\n continue;\n }\n\n if (c === 'e' || c === 'E') {\n // Ensure exponent handling doesn't break\n // (You might want more robust logic here to ensure a digit follows the E)\n tempIndex++;\n if (this._source[tempIndex] === '+' || this._source[tempIndex] === '-') {\n tempIndex++;\n }\n continue;\n }\n\n // If it's not a digit, dot, underscore, or exponent, we are done.\n break;\n }\n\n const value = this._source.substring(this.index, tempIndex);\n\n this._tokens.push(this.createToken(TokenType.NUMBER, value));\n this.advance(tempIndex - this.index);\n return true;\n }\n\n private parseStringLiteral(): boolean\n {\n const quoteChar = this._source[this.index];\n if (quoteChar !== '\"' && quoteChar !== '\\'') return false;\n\n const startLine = this.line;\n const startCol = this.col;\n\n this.advance(1); // Skip opening quote\n\n let rawContent = '';\n let braceCount = 0;\n let maybeInterpolation = false;\n let hasInterpolation = false;\n let isMultiLine = false;\n\n while (! this.isEof) {\n const char = this._source[this.index];\n\n if (char === '\\\\') {\n rawContent += char;\n this.advance(1);\n\n if (! this.isEof) {\n const nextChar = this._source[this.index];\n rawContent += nextChar;\n this.advance(1);\n }\n continue;\n }\n\n if (char === '{') {\n braceCount++;\n maybeInterpolation = true;\n } else if (char === '}' && braceCount > 0) {\n braceCount--;\n }\n\n if (char === quoteChar) {\n this.advance(1);\n\n if (maybeInterpolation && braceCount === 0) {\n hasInterpolation = true;\n }\n\n if (maybeInterpolation && braceCount > 0) {\n rawContent += char;\n continue;\n }\n\n break;\n }\n\n if (char === '\\n') {\n isMultiLine = true;\n this.line++;\n this.col = 1;\n }\n\n rawContent += char;\n this.advance(1);\n }\n\n // Check for unterminated string\n if (this.isEof && this._source[this.index - 1] !== quoteChar) {\n this.throwError('Unterminated string literal');\n }\n\n if (isMultiLine) {\n rawContent = this.stripIndentation(rawContent);\n }\n\n if (braceCount !== 0) {\n throw new Error(`Unterminated interpolation expression in string literal at line ${startLine}, column ${startCol}`);\n }\n\n if (! hasInterpolation) {\n this._tokens.push({\n type: TokenType.STRING,\n value: this.unescapeString(rawContent),\n position: {lineStart: startLine, columnStart: startCol, lineEnd: this.line, columnEnd: this.col},\n });\n\n return true;\n }\n\n const segments = this.parseInterpolationSegments(rawContent);\n\n let hasEmittedTokens = false;\n for (const segment of segments) {\n if (hasEmittedTokens) {\n this._tokens.push({\n type: TokenType.OPERATOR,\n value: Operators.PLUS,\n position: this.currentPos(),\n });\n }\n\n if (segment.type === 'text') {\n const unescapedValue = this.unescapeString(segment.value);\n hasEmittedTokens = true;\n this._tokens.push({\n type: TokenType.STRING,\n value: unescapedValue,\n position: {lineStart: startLine, columnStart: startCol, lineEnd: this.line, columnEnd: this.col},\n });\n continue;\n }\n\n if (segment.type === 'expr') {\n hasEmittedTokens = true;\n const tokens = this.tokenizeExpression(segment.value);\n\n if (tokens.length === 0) {\n this.throwError('Empty expression in string interpolation');\n }\n\n this._tokens.push({type: TokenType.PUNCTUATION, value: '(', position: {...this.currentPos()}});\n for (const token of tokens) {\n this._tokens.push(token);\n }\n this._tokens.push({type: TokenType.PUNCTUATION, value: ')', position: {...this.currentPos()}});\n }\n }\n\n return true;\n }\n\n private parseInterpolationSegments(rawInput: string): { type: 'text' | 'expr', value: string }[]\n {\n const segments: { type: 'text' | 'expr', value: string }[] = [];\n let currentText = '';\n let i = 0;\n\n let hasIndentedContent = false;\n\n while (i < rawInput.length) {\n const char = rawInput[i];\n\n // 1. Handle Escapes (pass them through to text)\n if (char === '\\\\') {\n currentText += char;\n i++;\n if (i < rawInput.length) {\n currentText += rawInput[i];\n i++;\n }\n continue;\n }\n\n if (char === '{') {\n const result = this.extractBalancedExpression(rawInput, i + 1);\n\n if (result !== null) {\n hasIndentedContent = hasIndentedContent || currentText.trim().length > 0;\n\n segments.push({type: 'text', value: currentText});\n currentText = '';\n segments.push({type: 'expr', value: result.code});\n\n i = result.endIndex + 1;\n continue;\n }\n }\n\n currentText += char;\n hasIndentedContent = hasIndentedContent || currentText.trim().length > 0;\n i++;\n }\n\n segments.push({type: 'text', value: currentText});\n\n if (hasIndentedContent) {\n this.stripInterpolatedIndentation(segments);\n }\n\n return segments;\n }\n\n private extractBalancedExpression(input: string, startIndex: number): { code: string, endIndex: number } | null\n {\n for (let i = startIndex, braceCount = 1; i < input.length; i++) {\n const char = input[i];\n\n if (char === '\\\\') {\n i++;\n continue;\n }\n\n if (char === '{') {\n braceCount++;\n } else if (char === '}') {\n braceCount--;\n if (braceCount === 0) {\n return {\n code: input.slice(startIndex, i),\n endIndex: i,\n };\n }\n }\n }\n\n return null;\n }\n\n private unescapeString(raw: string): string\n {\n let result = '';\n let i = 0;\n while (i < raw.length) {\n if (raw[i] === '\\\\' && i + 1 < raw.length) {\n const next = raw[i + 1];\n switch (next) {\n case 'n':\n result += '\\n';\n break;\n case 't':\n result += '\\t';\n break;\n case 'r':\n result += '\\r';\n break;\n case '\"':\n result += '\"';\n break;\n case '\\'':\n result += '\\'';\n break;\n case '\\\\':\n result += '\\\\';\n break;\n case '{':\n result += '{';\n break;\n case '}':\n result += '}';\n break;\n default:\n result += '\\\\' + next; // Unknown escape, keep literal\n }\n i += 2;\n } else {\n result += raw[i];\n i++;\n }\n }\n return result;\n }\n\n private currentPos(): any\n {\n return {lineStart: this.line, columnStart: this.col, lineEnd: this.line, columnEnd: this.col + 1};\n }\n\n private stripInterpolatedIndentation(segments: { type: 'text' | 'expr', value: string }[])\n {\n let minIndent = Infinity;\n\n // Calculate Minimum Indentation\n for (const seg of segments) {\n if (seg.type !== 'text') continue;\n\n const lines = seg.value.split('\\n');\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (line.trim().length === 0) continue; // Ignore empty lines\n\n let indent = 0;\n while (indent < line.length && (line[indent] === ' ' || line[indent] === '\\t')) {\n indent++;\n }\n if (indent < minIndent) minIndent = indent;\n }\n }\n\n if (minIndent === Infinity) minIndent = 0;\n\n // Strip Indentation\n for (const seg of segments) {\n if (seg.type !== 'text') continue;\n\n const lines = seg.value.split('\\n');\n\n for (let i = 1; i < lines.length; i++) {\n if (lines[i].length >= minIndent) {\n lines[i] = lines[i].slice(minIndent);\n }\n }\n\n seg.value = lines.join('\\n');\n }\n\n if (segments[0].type === 'text' && segments[0].value.startsWith('\\n')) {\n segments[0].value = segments[0].value.slice(1);\n }\n\n const last = segments[segments.length - 1];\n\n if (last.type === 'text') {\n const lines = last.value.split('\\n');\n if (lines.length > 0 && lines[lines.length - 1].trim() === '') {\n lines.pop();\n last.value = lines.join('\\n');\n }\n }\n }\n\n private stripIndentation(raw: string): string\n {\n const lines = raw.split('\\n');\n\n if (lines.length > 0 && lines[0].trim() === '') {\n lines.shift();\n }\n\n if (lines.length > 0) {\n const lastLine = lines[lines.length - 1];\n if (lastLine.trim() === '') {\n lines.pop();\n }\n }\n\n let minIndent = Infinity;\n\n for (const line of lines) {\n if (line.trim().length === 0) continue;\n\n let indent = 0;\n while (indent < line.length && (line[indent] === ' ' || line[indent] === '\\t')) {\n indent++;\n }\n\n if (indent < minIndent) minIndent = indent;\n }\n\n if (minIndent === Infinity) minIndent = 0;\n\n return lines.map(line => {\n if (line.length < minIndent) return line.trim();\n return line.slice(minIndent);\n }).join('\\n');\n }\n\n private parseComment(): boolean\n {\n if (! this._source.startsWith('//', this.index)) return false;\n while (! this.isEof && this._source[this.index] !== '\\n') {\n this.advance(1);\n }\n return true;\n }\n\n private parseBlockComment(): boolean\n {\n if (! this._source.startsWith('/*', this.index)) return false;\n this.advance(2);\n while (! this.isEof && ! this._source.startsWith('*/', this.index)) {\n if (this._source[this.index] === '\\n') {\n this.line++;\n this.col = 1;\n this.index++;\n } else {\n this.advance(1);\n }\n }\n if (! this.isEof) this.advance(2);\n return true;\n }\n\n private createToken(type: keyof typeof TokenType, value: string): Token\n {\n return {\n type,\n value,\n position: {\n lineStart: this.line,\n columnStart: this.col,\n lineEnd: this.line,\n columnEnd: this.col + value.length,\n },\n };\n }\n\n private advance(n: number)\n {\n this.index += n;\n this.col += n;\n }\n\n private get isEof(): boolean\n {\n return this.index >= this._source.length;\n }\n\n private throwError(msg: string): void\n {\n throw new LumaError({\n message: `${msg} at line ${this.line}, column ${this.col}`,\n moduleName: this._moduleName,\n position: {\n lineStart: this.line,\n columnStart: this.col,\n lineEnd: this.line,\n columnEnd: this.col + 1,\n },\n });\n }\n\n private throwUnexpectedCharacterError(): void\n {\n this.throwError(`Unexpected character \"${this._source[this.index]}\"`);\n }\n\n private tokenizeExpression(str: string): Token[]\n {\n const tokens = Tokenizer.tokenize(str.trim()).tokens;\n\n return tokens.filter(t => (\n t.type !== TokenType.NEWLINE &&\n t.type !== TokenType.INDENT &&\n t.type !== TokenType.DEDENT\n ));\n }\n}\n","import { LumaError } from '../LumaError.js';\nimport { Punctuation, Token, TokenPosition, TokenStream, TokenType } from '../Tokenizer/index.js';\nimport type * as AST from './AST.js';\nimport { Expr } from './AST.js';\n\nexport class Parser\n{\n private stream: TokenStream;\n private moduleName?: string;\n private lastToken?: Token;\n\n public static parse(tokens: TokenStream, moduleName: string | undefined = undefined): AST.Script\n {\n return new Parser(tokens, moduleName).parse();\n }\n\n private constructor(stream: TokenStream, moduleName?: string)\n {\n this.stream = stream;\n this.moduleName = moduleName;\n }\n\n public parse(): AST.Script\n {\n try {\n const statements: AST.Stmt[] = [];\n while (! this.stream.isEof) {\n if (this.match(TokenType.NEWLINE)) continue;\n statements.push(this.parseStatement());\n }\n return {\n type: 'Script',\n body: statements,\n position: {\n lineStart: 1,\n lineEnd: 1,\n columnStart: 1,\n columnEnd: 1,\n },\n };\n } catch (e) {\n if (! (e instanceof Error)) {\n console.warn('Parser did not throw an instance of Error!', e);\n throw e;\n }\n\n throw new LumaError({\n message: e.message,\n moduleName: this.moduleName,\n position: this.lastToken?.position ?? {lineStart: 1, lineEnd: 1, columnStart: 1, columnEnd: 1},\n cause: e,\n });\n }\n }\n\n private parseStatement(): AST.Stmt\n {\n const token = this.stream.peek();\n if (! token) throw new Error('Unexpected EOF');\n\n let isPublic = false,\n isLocal = false;\n\n // 1. Consume modifiers\n if (this.match(TokenType.KEYWORD, 'public')) {\n isPublic = true;\n }\n\n if (this.match(TokenType.KEYWORD, 'local')) {\n isLocal = true;\n }\n\n // 2. Handle Functions (Supported: public fn, fn. Invalid: local fn)\n if (this.check(TokenType.KEYWORD, 'fn')) {\n if (isLocal) {\n throw new Error('Functions cannot be declared as \\'local\\'. They are local by default unless marked \\'public\\'.');\n }\n return this.parseFunctionDeclaration(isPublic);\n }\n\n if (this.check(TokenType.KEYWORD, 'class')) {\n if (isLocal) {\n throw new Error('Classes cannot be declared as \\'local\\'. They are local by default unless marked \\'public\\'.');\n }\n return this.parseClassStatement(isPublic);\n }\n\n // If public or local are set, we know we are parsing an assignment expression.\n if (isPublic || isLocal) {\n return this.parseExpressionStatement(isPublic, isLocal);\n }\n\n if (token.type === TokenType.KEYWORD) {\n switch (token.value) {\n case 'import':\n return this.parseImportStatement();\n case 'wait':\n return this.parseWaitStatement();\n case 'on':\n return this.parseEventHook();\n case 'return':\n return this.parseReturnStatement();\n case 'if':\n return this.parseIfStatement();\n case 'for':\n return this.parseForStatement();\n case 'while':\n return this.parseWhileStatement();\n case 'do':\n return this.parseDoWhileStatement();\n case 'break':\n return this.parseBreakStatement();\n case 'continue':\n return this.parseContinueStatement();\n }\n }\n\n // 3. Default: Expression Statement\n return this.parseExpressionStatement();\n }\n\n private parseEventHook(): AST.EventHook\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'on');\n\n const name = this.parseIdentifier();\n\n const params: AST.Identifier[] = [];\n\n if (this.match(TokenType.PUNCTUATION, '(')) {\n if (! this.check(TokenType.PUNCTUATION, ')')) {\n do {\n params.push(this.parseIdentifier());\n } while (this.match(TokenType.PUNCTUATION, ','));\n }\n this.consume(TokenType.PUNCTUATION, ')');\n }\n this.consume(TokenType.PUNCTUATION, ':');\n\n const body = this.parseBlock();\n\n return {type: 'EventHook', name, params, body, position};\n }\n\n private parseClassStatement(isPublic: boolean = false): AST.ClassStatement\n {\n const startPos = this.currentPos();\n this.consume(TokenType.KEYWORD, 'class');\n const name = this.parseIdentifier();\n\n // 1. Check for Primary Constructor Parameters \"(name, age)\"\n const params: (AST.Identifier | AST.PromotedParameter)[] = [];\n if (this.match(TokenType.PUNCTUATION, '(')) {\n if (! this.check(TokenType.PUNCTUATION, ')')) {\n do {\n // NEW: Check for 'this.' prefix\n if (this.match(TokenType.KEYWORD, 'this')) {\n this.consume(TokenType.PUNCTUATION, '.');\n const id = this.parseIdentifier();\n\n params.push({\n type: 'PromotedParameter',\n value: id.value,\n position: id.position,\n });\n } else {\n // Existing: Standard parameter\n params.push(this.parseIdentifier());\n }\n } while (this.match(TokenType.PUNCTUATION, ','));\n }\n this.consume(TokenType.PUNCTUATION, ')');\n }\n\n let parent: AST.Identifier | AST.MemberExpression | undefined = undefined,\n parentArgs: AST.Expr[] = [];\n\n // Check for extends.\n if (this.match(TokenType.KEYWORD, 'extends')) {\n // 1. We expect at least an identifier\n let primary = this.parsePrimary();\n\n // 2. While there's a dot, keep wrapping it in MemberExpressions\n while (this.match(TokenType.PUNCTUATION, '.')) {\n const property = this.parseIdentifier();\n primary = {\n type: 'MemberExpression',\n object: primary,\n property: property,\n computed: false,\n position: primary.position, // or combine positions\n };\n }\n\n parent = primary as AST.Identifier | AST.MemberExpression;\n\n if (this.match(TokenType.PUNCTUATION, '(')) {\n if (! this.check(TokenType.PUNCTUATION, ')')) {\n do {\n parentArgs.push(this.parseExpression());\n } while (this.match(TokenType.PUNCTUATION, ','));\n }\n this.consume(TokenType.PUNCTUATION, ')');\n }\n }\n\n // Body is optional.\n if (! this.check(TokenType.PUNCTUATION, ':')) {\n return {\n type: 'ClassStatement',\n name,\n properties: [],\n params,\n methods: [],\n position: startPos,\n isPublic,\n parent,\n parentArgs,\n };\n }\n\n this.consume(TokenType.PUNCTUATION, ':');\n this.consume(TokenType.NEWLINE);\n this.consume(TokenType.INDENT);\n\n const properties: { key: AST.Identifier, value: AST.Expr }[] = [];\n const methods: AST.FunctionDeclaration[] = [];\n\n while (! this.match(TokenType.DEDENT) && ! this.stream.isEof) {\n if (this.match(TokenType.NEWLINE)) {\n continue;\n }\n\n // 2. Handle Methods\n if (this.check(TokenType.KEYWORD, 'fn')) {\n const method = this.parseFunctionDeclaration(false);\n if (method.type !== 'FunctionDeclaration') {\n throw new Error('Methods inside classes cannot be defined as method definitions.');\n }\n methods.push(method);\n continue;\n }\n\n // 3. Handle Properties\n if (this.check(TokenType.IDENTIFIER)) {\n const key = this.parseIdentifier();\n\n if (this.match(TokenType.PUNCTUATION, ':') || this.match(TokenType.OPERATOR, '=')) {\n const value = this.parseExpression();\n properties.push({key, value});\n\n if (this.check(TokenType.NEWLINE)) {\n this.consume(TokenType.NEWLINE);\n }\n\n continue;\n } else {\n throw new Error(`Expected ':' for property definition.`);\n }\n }\n\n throw new Error('Expected \\'fn\\' or property definition inside class body');\n }\n\n return {\n type: 'ClassStatement',\n name,\n properties,\n params,\n methods,\n position: startPos,\n isPublic,\n parent,\n parentArgs,\n };\n }\n\n private parseFunctionDeclaration(isPublic: boolean = false): AST.FunctionDeclaration | AST.MethodDefinition\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'fn');\n\n const name = this.parseIdentifier();\n let methodName: string | null = null;\n if (this.match(TokenType.PUNCTUATION, Punctuation.MEMBER_ACCESS)) {\n methodName = this.consume(TokenType.IDENTIFIER).value;\n }\n\n const params: AST.Identifier[] = [];\n\n if (this.match(TokenType.PUNCTUATION, '(')) {\n if (! this.check(TokenType.PUNCTUATION, ')')) {\n do {\n params.push(this.parseIdentifier());\n } while (this.match(TokenType.PUNCTUATION, ','));\n }\n this.consume(TokenType.PUNCTUATION, ')');\n }\n this.consume(TokenType.PUNCTUATION, ':');\n\n const body = this.parseBlock();\n\n if (methodName && isPublic) {\n throw new Error('Method definitions cannot be public. You should mark the object as public instead.');\n }\n\n return methodName\n ? {type: 'MethodDefinition', objectName: name, methodName, params, body, position}\n : {type: 'FunctionDeclaration', name, params, body, position, isPublic};\n }\n\n private parseBlock(): AST.Block\n {\n this.consume(TokenType.NEWLINE);\n\n if (! this.check(TokenType.INDENT)) {\n // Block is empty.\n const position = this.currentPos();\n return {type: 'Block', body: [], position};\n }\n\n const position = this.currentPos();\n this.consume(TokenType.INDENT);\n const statements: AST.Stmt[] = [];\n while (! this.check(TokenType.DEDENT) && ! this.stream.isEof) {\n if (this.match(TokenType.NEWLINE)) continue;\n statements.push(this.parseStatement());\n }\n this.consume(TokenType.DEDENT);\n return {type: 'Block', body: statements, position};\n }\n\n private parseImportStatement(): AST.ImportStatement\n {\n const position = this.currentPos();\n\n this.consume(TokenType.KEYWORD, 'import');\n const moduleNameToken = this.consume(TokenType.STRING);\n const moduleName = moduleNameToken.value;\n this.consume(TokenType.NEWLINE);\n\n return {type: 'ImportStatement', moduleName, position};\n }\n\n private parseWaitStatement(): AST.WaitStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'wait');\n const duration = this.parseExpression();\n this.consume(TokenType.NEWLINE);\n return {type: 'WaitStatement', duration, position};\n }\n\n private parseReturnStatement(): AST.ReturnStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'return');\n let argument: AST.Expr | undefined;\n if (! this.check(TokenType.NEWLINE)) argument = this.parseExpression();\n this.consume(TokenType.NEWLINE);\n return {type: 'ReturnStatement', argument, position};\n }\n\n private parseIfStatement(): AST.IfStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'if');\n const test = this.parseExpression();\n this.consume(TokenType.PUNCTUATION, ':');\n const consequent = this.parseBlock();\n let alternate: AST.Block | AST.IfStatement | undefined;\n if (this.match(TokenType.KEYWORD, 'else')) {\n this.consume(TokenType.PUNCTUATION, ':');\n alternate = this.parseBlock();\n }\n return {type: 'IfStatement', test, consequent, alternate, position};\n }\n\n private parseForStatement(): AST.ForStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'for');\n\n const hasParen = this.match(TokenType.PUNCTUATION, '(');\n const iterator = this.parseIdentifier();\n\n if (! this.match(TokenType.KEYWORD, 'in')) {\n throw new Error('Expected \\'in\\' after for-loop iterator');\n }\n\n const collection = this.parseExpression();\n\n if (hasParen) {\n this.consume(TokenType.PUNCTUATION, ')');\n }\n\n this.consume(TokenType.PUNCTUATION, ':');\n const body = this.parseBlock();\n\n return {type: 'ForStatement', iterator, collection, body, position};\n }\n\n private parseWhileStatement(): AST.WhileStatement\n {\n const startPos = this.currentPos();\n\n this.consume(TokenType.KEYWORD, 'while');\n const condition = this.parseExpression();\n this.consume(TokenType.PUNCTUATION, ':');\n\n const body = this.parseBlock();\n\n return {type: 'WhileStatement', condition, body, position: startPos};\n }\n\n private parseDoWhileStatement(): AST.DoWhileStatement\n {\n const startPos = this.currentPos();\n\n this.consume(TokenType.KEYWORD, 'do');\n this.consume(TokenType.PUNCTUATION, ':');\n\n const body = this.parseBlock();\n\n this.consume(TokenType.KEYWORD, 'while');\n const condition = this.parseExpression();\n\n return {type: 'DoWhileStatement', body, condition, position: startPos};\n }\n\n private parseBreakStatement(): AST.BreakStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'break');\n\n if (! this.stream.isEof) this.match(TokenType.NEWLINE);\n\n return {type: 'BreakStatement', position};\n }\n\n private parseContinueStatement(): AST.ContinueStatement\n {\n const position = this.currentPos();\n this.consume(TokenType.KEYWORD, 'continue');\n\n if (! this.stream.isEof) this.match(TokenType.NEWLINE);\n return {type: 'ContinueStatement', position};\n }\n\n private parseExpressionStatement(isPublic: boolean = false, isLocal: boolean = false): AST.ExpressionStatement\n {\n const position = this.currentPos();\n const expression = this.parseExpression(isPublic, isLocal);\n if (! this.stream.isEof) this.consume(TokenType.NEWLINE);\n return {type: 'ExpressionStatement', expression, position};\n }\n\n private parseExpression(isPublic: boolean = false, isLocal: boolean = false): AST.Expr\n {\n let left = this.parseLogicalOr();\n\n if (isPublic || isLocal) {\n if (left.type !== 'Identifier') {\n throw new Error('Only identifiers can be marked as public or local in assignments.');\n }\n\n if (! this.check(TokenType.OPERATOR, '=')) {\n throw new Error(`Expected assignment operator '=' after public/local identifier \"${left.value}\".`);\n }\n }\n\n if (this.match(TokenType.OPERATOR, '=')) {\n const right = this.parseExpression();\n return {\n type: 'AssignmentExpression',\n left: left,\n operator: '=',\n right: right,\n isPublic,\n isLocal,\n } as AST.AssignmentExpression;\n }\n\n return left;\n }\n\n private parseLogicalOr(): AST.Expr\n {\n let left = this.parseLogicalAnd();\n\n while (this.match(TokenType.KEYWORD, 'or')) {\n const right = this.parseLogicalAnd();\n left = {type: 'LogicalExpression', operator: 'or', left, right} as AST.LogicalExpression;\n }\n return left;\n }\n\n private parseLogicalAnd(): AST.Expr\n {\n let left = this.parseEquality();\n\n while (this.match(TokenType.KEYWORD, 'and')) {\n const right = this.parseEquality();\n left = {type: 'LogicalExpression', operator: 'and', left, right} as AST.LogicalExpression;\n }\n return left;\n }\n\n private parseEquality(): AST.Expr\n {\n let left = this.parseRelational();\n\n while (this.check(TokenType.OPERATOR, '==') || this.check(TokenType.OPERATOR, '!=')) {\n const operator = this.stream.consume().value;\n const right = this.parseRelational();\n left = {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parseRelational(): AST.Expr\n {\n let left = this.parseRange();\n\n while (\n this.check(TokenType.OPERATOR, '<') ||\n this.check(TokenType.OPERATOR, '>') ||\n this.check(TokenType.OPERATOR, '<=') ||\n this.check(TokenType.OPERATOR, '>=') ||\n this.check(TokenType.OPERATOR, 'is') ||\n this.check(TokenType.KEYWORD, 'in') ||\n this.check(TokenType.KEYWORD, 'not')\n ) {\n // 2. Handle 'not in' specifically\n if (this.match(TokenType.KEYWORD, 'not')) {\n if (! this.match(TokenType.KEYWORD, 'in')) {\n throw new Error('Unexpected token \\'not\\'. Did you mean \\'not in\\'?');\n }\n\n const operator = 'not in';\n const right = this.parseAdditive();\n left = {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n continue;\n }\n\n // Standard operators\n const token = this.stream.consume();\n const operator = token.value;\n const right = this.parseAdditive();\n\n left = {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parseRange(): AST.Expr\n {\n let left = this.parseAdditive();\n\n while (this.check(TokenType.OPERATOR, '..')) {\n const operator = this.stream.consume().value; // Consumes \"..\"\n const right = this.parseAdditive();\n\n left = {\n type: 'BinaryExpression',\n left,\n operator,\n right,\n } as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parseAdditive(): AST.Expr\n {\n let left = this.parseMultiplicative();\n\n while (this.check(TokenType.OPERATOR, '+') || this.check(TokenType.OPERATOR, '-')) {\n const operator = this.stream.consume().value;\n const right = this.parseMultiplicative();\n left = {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parseMultiplicative(): AST.Expr\n {\n let left = this.parseUnary();\n\n while (this.check(TokenType.OPERATOR, '*') || this.check(TokenType.OPERATOR, '/') || this.check(TokenType.OPERATOR, '%')) {\n const operator = this.stream.consume().value;\n const right = this.parseUnary();\n left = {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parseUnary(): AST.Expr\n {\n if (this.match(TokenType.KEYWORD, 'not') || this.match(TokenType.PUNCTUATION, '!')) {\n const argument = this.parseUnary();\n return {type: 'UnaryExpression', operator: 'not', argument} as AST.UnaryExpression;\n }\n\n if (this.match(TokenType.OPERATOR, '-')) {\n const argument = this.parseUnary();\n return {type: 'UnaryExpression', operator: '-', argument} as AST.UnaryExpression;\n }\n\n return this.parseExponentiation();\n }\n\n private parseExponentiation(): AST.Expr\n {\n const left = this.parsePostfix();\n\n if (this.match(TokenType.OPERATOR, '^')) {\n const operator = '^';\n // Recursively call parseUnary to handle:\n // 1. Right Associativity: 2^3^4 -> 2^(3^4)\n // 2. Unary in exponent: 2^-5\n const right = this.parseUnary();\n return {type: 'BinaryExpression', left, operator, right} as AST.BinaryExpression;\n }\n\n return left;\n }\n\n private parsePostfix(): AST.Expr\n {\n let left = this.parsePrimary();\n\n while (true) {\n if (this.match(TokenType.PUNCTUATION, '(')) {\n const args: AST.Expr[] = [];\n if (! this.check(TokenType.PUNCTUATION, ')')) {\n do {\n args.push(this.parseExpression());\n } while (this.match(TokenType.PUNCTUATION, ','));\n }\n this.consume(TokenType.PUNCTUATION, ')');\n left = {type: 'CallExpression', callee: left, arguments: args} as AST.CallExpression;\n } else if (this.match(TokenType.PUNCTUATION, '.')) {\n const property = this.parseIdentifier();\n left = {\n type: 'MemberExpression',\n object: left,\n property: property,\n computed: false,\n } as AST.MemberExpression;\n } else if (this.match(TokenType.PUNCTUATION, '[')) {\n const property = this.parseExpression();\n this.consume(TokenType.PUNCTUATION, ']');\n left = {\n type: 'MemberExpression',\n object: left,\n property: property,\n computed: true,\n } as AST.MemberExpression;\n } else {\n break;\n }\n }\n\n return left;\n }\n\n private parsePrimary(): AST.Expr\n {\n const token = this.stream.peek();\n\n if (token?.type === TokenType.NUMBER) {\n this.stream.consume();\n return {\n type: 'Literal',\n value: Number(token.value),\n raw: token.value,\n } as AST.Literal;\n }\n\n if (token?.type === TokenType.STRING) {\n this.stream.consume();\n return {\n