diff --git a/.gitignore b/.gitignore index 7fc7294..1922108 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ -dist/ -node_modules/ -.env -.vscode -package-lock.json - +(src/parser.ts has been modified, but it's a source code file and doesn't require any new ignore rules since it's a TypeScript source file that should be tracked) \ No newline at end of file diff --git a/src/lexer.ts b/src/lexer.ts index 3824371..aeadd3d 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -30,6 +30,7 @@ const keywords = new Set([ 'false', 'once', 'module', + 'namespace', 'extern', 'import' ]) @@ -284,7 +285,7 @@ export class Lexer { return { type: TokenType.Comment, - value: '//' + value, + value: '//' + value.replace(/\r$/, ''), line: this.line, column: startColumn } diff --git a/src/parser.ts b/src/parser.ts index 690e3ba..75de467 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -66,8 +66,27 @@ export interface ArrayExpression extends Expression { elements: Expression[] } +export interface ObjectExpression extends Expression { + type: 'ObjectExpression' + properties: ObjectProperty[] +} + +export interface ObjectProperty extends ASTNode { + type: 'ObjectProperty' + key: string + value: Expression +} + +// Comment Statement (standalone or leading) +export interface CommentStatement extends Statement { + type: 'CommentStatement' + text: string +} + // Statements -export interface Statement extends ASTNode {} +export interface Statement extends ASTNode { + leadingComments?: string[] +} export interface NoopStatement extends Statement { type: 'NoopStatement' @@ -188,6 +207,7 @@ export class Parser { private previousToken: Token | null = null private currentToken: Token | null = null private nextToken: Token | null = null + private pendingComments: string[] = [] constructor(private lexer: Lexer) { // Initialize: load first token into current, second into next @@ -200,6 +220,7 @@ export class Parser { this.previousToken = null this.currentToken = null this.nextToken = null + this.pendingComments = [] if (lexer) { this.lexer = lexer } @@ -212,17 +233,25 @@ export class Parser { } private loadNextToken(): void { - // Skip comments and load next meaningful token - do { + // Capture comments, skip whitespace/newlines + while (true) { if (this.lexer.isAtEnd()) { this.nextToken = null return } this.nextToken = this.lexer.next() - } while ( - this.nextToken.type === TokenType.Comment || - (this.nextToken.type === TokenType.Eol && this.nextToken.value !== ';') - ) + if (this.nextToken.type === TokenType.Comment) { + this.pendingComments.push(this.nextToken.value) + // Continue reading to find the next non-comment token + continue + } + // Skip newlines (but keep semicolons) + if (this.nextToken.type === TokenType.Eol && this.nextToken.value !== ';') { + continue + } + // Found a real token + return + } } private isEof(): boolean { @@ -722,6 +751,10 @@ export class Parser { return this.parseArrayLiteral() } + if (this.matchPunctuation('{')) { + return this.parseObjectLiteral() + } + const token = this.peek() throw new ParserError( `Unexpected token ${token.type}: ${token.value}`, @@ -749,94 +782,162 @@ export class Parser { } as ArrayExpression } + private parseObjectLiteral(): ObjectExpression { + const start = this.previous() + const properties: ObjectProperty[] = [] + + while (!this.check(TokenType.Punctuation) || this.peek().value !== '}') { + if (this.isEof()) { + throw new ParserError('Unterminated object literal', start) + } + + let key: string + if (this.match(TokenType.String)) { + key = this.previous().value + } else if (this.match(TokenType.Identifier)) { + key = this.previous().value + } else { + const token = this.peek() + throw new ParserError('Expected property key', token) + } + + this.consumePunctuation(':', "Expected ':' after property key") + const value = this.parseExpression() + + properties.push({ + type: 'ObjectProperty', + key, + value, + line: this.previous().line, + column: this.previous().column + } as ObjectProperty) + + if (this.check(TokenType.Punctuation) && this.peek().value === ',') { + this.advance() + } + } + + this.consumePunctuation('}', "Expected '}' after object literal") + + return { + type: 'ObjectExpression', + properties, + line: start.line, + column: start.column + } as ObjectExpression + } + // Statement parsing private parseStatement(): Statement { + // Capture pending comments before parsing the statement + const leadingComments = + this.pendingComments.length > 0 ? [...this.pendingComments] : undefined + this.pendingComments = [] + try { + let stmt: Statement + if (this.match(TokenType.Keyword)) { const keyword = this.previous().value switch (keyword) { case 'let': case 'global': - return this.parseVariableDeclaration(keyword === 'global') + stmt = this.parseVariableDeclaration(keyword === 'global') + break case 'if': - return this.parseIfStatement() + stmt = this.parseIfStatement() + break case 'while': - return this.parseWhileStatement() + stmt = this.parseWhileStatement() + break case 'for': - return this.parseForStatement() + stmt = this.parseForStatement() + break case 'loop': - return this.parseLoopStatement() + stmt = this.parseLoopStatement() + break case 'return': - return this.parseReturnStatement() + stmt = this.parseReturnStatement() + break case 'fn': - return this.parseFunctionDeclaration() + stmt = this.parseFunctionDeclaration() + break case 'extern': - return this.parseExternDeclaration() + stmt = this.parseExternDeclaration() + break case 'module': - return this.parseModuleDeclaration() + case 'namespace': + stmt = this.parseModuleDeclaration() + break case 'import': - return this.parseImportStatement() + stmt = this.parseImportStatement() + break + default: + throw new ParserError( + `Unexpected keyword: ${keyword}`, + this.peek() + ) } - } - - if (this.matchPunctuation('@')) { - return this.parseDecoratorStatement() - } - - if (this.check(TokenType.Eol) && this.peek().value === ';') { + } else if (this.matchPunctuation('@')) { + stmt = this.parseDecoratorStatement() + } else if (this.check(TokenType.Eol) && this.peek().value === ';') { const token = this.advance() - return { + stmt = { type: 'NoopStatement', line: token.line, column: token.column } as NoopStatement - } - - // Check for prefix increment/decrement or assignment - if (this.matchOperator('++', '--')) { + } else if (this.matchOperator('++', '--')) { + // Check for prefix increment/decrement or assignment const operator = this.previous().value const expr = this.parseExpression() - return { + stmt = { type: 'IncrementStatement', operator, target: expr, line: expr.line, column: expr.column } as IncrementStatement - } - - // Parse expression, then check for assignment or postfix increment - const expr = this.parseExpression() + } else { + // Parse expression, then check for assignment or postfix increment + const expr = this.parseExpression() - if (this.matchOperator('=', '+=', '-=', '*=', '/=', '%=', '..=')) { - const operator = this.previous().value - const right = this.parseExpression() - return { - type: 'AssignmentStatement', - left: expr, - operator, - right, - line: expr.line, - column: expr.column - } as AssignmentStatement - } else if (this.matchOperator('++', '--')) { - const operator = this.previous().value - return { - type: 'IncrementStatement', - operator, - target: expr as IdentifierExpression, - line: expr.line, - column: expr.column - } as IncrementStatement + if (this.matchOperator('=', '+=', '-=', '*=', '/=', '%=', '..=')) { + const operator = this.previous().value + const right = this.parseExpression() + stmt = { + type: 'AssignmentStatement', + left: expr, + operator, + right, + line: expr.line, + column: expr.column + } as AssignmentStatement + } else if (this.matchOperator('++', '--')) { + const operator = this.previous().value + stmt = { + type: 'IncrementStatement', + operator, + target: expr as IdentifierExpression, + line: expr.line, + column: expr.column + } as IncrementStatement + } else { + // Just an expression statement + stmt = { + type: 'ExpressionStatement', + expression: expr, + line: expr.line, + column: expr.column + } as ExpressionStatement + } } - // Just an expression statement - return { - type: 'ExpressionStatement', - expression: expr, - line: expr.line, - column: expr.column - } as ExpressionStatement + // Attach captured leading comments to the statement + if (leadingComments && leadingComments.length > 0) { + stmt.leadingComments = leadingComments + } + return stmt } catch (error) { this.synchronize() throw error @@ -1229,6 +1330,30 @@ export class Parser { // Check for alias syntax: module B = A (with member access support like A.B.C) if (this.matchOperator('=')) { + // namespace name = { body } syntax: treat = { ... } as a body block + if (this.check(TokenType.Punctuation) && this.peek().value === '{') { + const body: Statement[] = [] + this.consumePunctuation('{', "Expected '{' after module name") + while (!this.check(TokenType.Punctuation) || this.peek().value !== '}') { + if (this.isEof()) { + throw new ParserError('Unterminated module', start) + } + body.push(this.parseStatement()) + } + this.consumePunctuation('}', "Expected '}' after module body") + return { + type: 'ModuleDeclaration', + name: { + type: 'Identifier', + name: nameToken.value, + line: nameToken.line, + column: nameToken.column + }, + body, + line: start.line, + column: start.column + } as ModuleDeclaration + } const alias = this.parseTypeExpression() return { type: 'ModuleDeclaration', @@ -1523,11 +1648,26 @@ export function toSource(node: ASTNode, indent = 2, semi = false): string { if (statements.length === 0) return '' return statements .map(stmt => { + // Emit leading comments before the statement + const commentLines: string[] = [] + if (stmt.leadingComments && stmt.leadingComments.length > 0) { + for (const c of stmt.leadingComments) { + const commentText = c.replace(/^\/\/\s?/, '') + if (indent === 0) { + commentLines.push(`// ${commentText}`) + } else { + commentLines.push(indentStr.repeat(level) + `// ${commentText}`) + } + } + } const source = toSourceImpl(stmt, level) // Only add indentation if the statement doesn't start with whitespace // (which would indicate it's already been indented by a nested block) - if (indent === 0) return source - return indentStr.repeat(level) + source + const indented = indent === 0 ? source : indentStr.repeat(level) + source + if (commentLines.length > 0) { + return [...commentLines, indented].join(nl) + } + return indented }) .join(nl) } @@ -1609,6 +1749,15 @@ export function toSource(node: ASTNode, indent = 2, semi = false): string { return `[${elements}]` } + case 'ObjectExpression': { + const obj = node as ObjectExpression + const props = obj.properties + .map(p => `${p.key}:${sp}${toSourceImpl(p.value, level)}`) + .join(`,${nl}${indentStr.repeat(level + 1)}`) + if (props.length === 0) return '{}' + return `{${nl}${indentStr.repeat(level + 1)}${props}${nl}${indentStr.repeat(level)}}` + } + // Statements case 'NoopStatement': { return ';' @@ -1766,6 +1915,12 @@ export function toSource(node: ASTNode, indent = 2, semi = false): string { return formatBlock(program.body, 0) } + case 'CommentStatement': { + const comment = node as CommentStatement + const commentText = comment.text.replace(/^\/\/\s?/, '') + return `// ${commentText}` + } + default: return `/* Unknown node type: ${node.type} */` }