botserver/packages/basic.gblib/services/GBTranspiler.txt
2024-12-10 15:46:18 -03:00

2550 lines
65 KiB
Text

import { GBVMService } from './GBVMService';
import path from 'path';
interface Token {
type: string;
value: string;
line: number;
column: number;
}
interface ASTNode {
type: string;
value?: any;
children?: ASTNode[];
line: number;
column: number;
}
export class GBCompiler {
private source: string = '';
private tokens: Token[] = [];
private currentToken = 0;
private line = 1;
private column = 1;
private output: string[] = [];
private inString = false;
private stringChar = '';
private buffer = '';
private lineMap: {[key: number]: number} = {};
private tasks: any[] = [];
// Internal state tracking
private inTalkBlock = false;
private talkBuffer = '';
private inSystemPrompt = false;
private systemPromptBuffer = '';
private currentTable: any = null;
constructor() {
// Initialize state
}
public compile(source: string, options: {
filename: string;
mainName: string;
pid: string;
}): {
code: string;
map: {[key: number]: number};
metadata: any;
tasks: any[];
systemPrompt: string;
} {
this.source = source;
this.reset();
// Compilation phases
this.tokenize();
const ast = this.parse();
this.generateCode(ast);
return {
code: this.output.join('\n'),
map: this.lineMap,
metadata: this.generateMetadata(options.mainName),
tasks: this.tasks,
systemPrompt: this.systemPromptBuffer
};
}
private reset() {
this.tokens = [];
this.currentToken = 0;
this.line = 1;
this.column = 1;
this.output = [];
this.lineMap = {};
this.tasks = [];
this.inTalkBlock = false;
this.talkBuffer = '';
this.inSystemPrompt = false;
this.systemPromptBuffer = '';
this.currentTable = null;
}
private tokenize() {
let current = 0;
const source = this.source;
while (current < source.length) {
let char = source[current];
// Handle whitespace
if (/\s/.test(char)) {
if (char === '\n') {
this.line++;
this.column = 1;
if (this.buffer) {
this.addToken('WORD', this.buffer);
this.buffer = '';
}
this.addToken('NEWLINE', '\n');
}
current++;
this.column++;
continue;
}
// Handle strings
if (char === '"' || char === "'" || char === '`') {
if (this.buffer) {
this.addToken('WORD', this.buffer);
this.buffer = '';
}
const stringValue = this.readString(source, current, char);
this.addToken('STRING', stringValue.value);
current = stringValue.position;
continue;
}
// Handle operators
if ('=()[]{}+-*/%<>!&|,'.includes(char)) {
if (this.buffer) {
this.addToken('WORD', this.buffer);
this.buffer = '';
}
const operator = this.readOperator(source, current);
this.addToken('OPERATOR', operator.value);
current = operator.position;
continue;
}
// Build up word buffer
this.buffer += char;
current++;
this.column++;
}
// Add any remaining buffer
if (this.buffer) {
this.addToken('WORD', this.buffer);
}
this.addToken('EOF', '');
}
private readString(source: string, start: number, quote: string) {
let value = '';
let position = start + 1;
let escaped = false;
while (position < source.length) {
const char = source[position];
if (escaped) {
value += char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
position++;
break;
} else {
value += char;
}
position++;
}
return { value, position };
}
private readOperator(source: string, start: number) {
const twoCharOperators = ['==', '!=', '>=', '<=', '&&', '||'];
const oneChar = source[start];
const twoChar = oneChar + (source[start + 1] || '');
if (twoCharOperators.includes(twoChar)) {
return {
value: twoChar,
position: start + 2
};
}
return {
value: oneChar,
position: start + 1
};
}
private addToken(type: string, value: string) {
this.tokens.push({
type,
value,
line: this.line,
column: this.column
});
}
private parse(): ASTNode {
const program: ASTNode = {
type: 'Program',
children: [],
line: 1,
column: 1
};
while (this.currentToken < this.tokens.length) {
const statement = this.parseStatement();
if (statement) {
program.children.push(statement);
}
}
return program;
}
private parseWhileStatement(): ASTNode {
this.currentToken++; // Skip DO
if (this.peek().value.toUpperCase() !== 'WHILE') {
throw new Error('Expected WHILE after DO');
}
this.currentToken++; // Skip WHILE
const condition = this.parseCondition();
const body = this.parseBlock();
// Must end with LOOP
if (this.peek().value.toUpperCase() !== 'LOOP') {
throw new Error('Expected LOOP at end of while statement');
}
this.currentToken++;
return {
type: 'WhileStatement',
value: {
condition,
body
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseExpression(): ASTNode {
return this.parseAssignment();
}
private parseAssignment(): ASTNode {
const left = this.parseLogicalOr();
if (this.peek().value === '=') {
this.currentToken++;
const right = this.parseAssignment();
return {
type: 'AssignmentExpression',
value: { left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseLogicalOr(): ASTNode {
let left = this.parseLogicalAnd();
while (
this.peek().value.toUpperCase() === 'OR' ||
this.peek().value === '||'
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const right = this.parseLogicalAnd();
left = {
type: 'LogicalExpression',
value: { operator: '||', left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseLogicalAnd(): ASTNode {
let left = this.parseEquality();
while (
this.peek().value.toUpperCase() === 'AND' ||
this.peek().value === '&&'
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const right = this.parseEquality();
left = {
type: 'LogicalExpression',
value: { operator: '&&', left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseEquality(): ASTNode {
let left = this.parseRelational();
while (
this.peek().value === '=' ||
this.peek().value === '<>' ||
this.peek().value === '==' ||
this.peek().value === '!='
) {
let operator = this.tokens[this.currentToken].value;
this.currentToken++;
// Convert BASIC operators to JS
if (operator === '=') operator = '===';
if (operator === '<>') operator = '!==';
const right = this.parseRelational();
left = {
type: 'BinaryExpression',
value: { operator, left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseRelational(): ASTNode {
let left = this.parseAdditive();
while (
this.peek().value === '<' ||
this.peek().value === '>' ||
this.peek().value === '<=' ||
this.peek().value === '>='
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const right = this.parseAdditive();
left = {
type: 'BinaryExpression',
value: { operator, left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseAdditive(): ASTNode {
let left = this.parseMultiplicative();
while (
this.peek().value === '+' ||
this.peek().value === '-'
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const right = this.parseMultiplicative();
left = {
type: 'BinaryExpression',
value: { operator, left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseMultiplicative(): ASTNode {
let left = this.parseUnary();
while (
this.peek().value === '*' ||
this.peek().value === '/' ||
this.peek().value === '%'
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const right = this.parseUnary();
left = {
type: 'BinaryExpression',
value: { operator, left, right },
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return left;
}
private parseUnary(): ASTNode {
if (
this.peek().value === '-' ||
this.peek().value === '!' ||
this.peek().value.toUpperCase() === 'NOT'
) {
const operator = this.tokens[this.currentToken].value;
this.currentToken++;
const argument = this.parseUnary();
return {
type: 'UnaryExpression',
value: {
operator: operator.toUpperCase() === 'NOT' ? '!' : operator,
argument
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return this.parsePrimary();
}
private parsePrimary(): ASTNode {
const token = this.peek();
switch (token.type) {
case 'NUMBER':
this.currentToken++;
return {
type: 'NumberLiteral',
value: parseFloat(token.value),
line: token.line,
column: token.column
};
case 'STRING':
this.currentToken++;
return {
type: 'StringLiteral',
value: token.value,
line: token.line,
column: token.column
};
case 'WORD':
// Check for special keywords
switch (token.value.toUpperCase()) {
case 'TRUE':
case 'FALSE':
this.currentToken++;
return {
type: 'BooleanLiteral',
value: token.value.toUpperCase() === 'TRUE',
line: token.line,
column: token.column
};
case 'NULL':
this.currentToken++;
return {
type: 'NullLiteral',
value: null,
line: token.line,
column: token.column
};
default:
// Check if it's a function call
if (this.tokens[this.currentToken + 1]?.value === '(') {
return this.parseFunctionCall();
}
// Otherwise it's an identifier
this.currentToken++;
return {
type: 'Identifier',
value: token.value,
line: token.line,
column: token.column
};
}
case 'LEFT_PAREN':
this.currentToken++; // Skip (
const expr = this.parseExpression();
if (this.peek().value !== ')') {
throw new Error('Expected closing parenthesis');
}
this.currentToken++; // Skip )
return expr;
default:
throw new Error(
`Unexpected token ${token.type} "${token.value}" at line ${token.line} column ${token.column}`
);
}
}
private parseFunctionCall(): ASTNode {
const identifier = this.parseIdentifier();
if (this.peek().value !== '(') {
throw new Error('Expected ( after function name');
}
this.currentToken++; // Skip (
const args = [];
while (this.peek().value !== ')') {
args.push(this.parseExpression());
if (this.peek().value === ',') {
this.currentToken++;
} else if (this.peek().value !== ')') {
throw new Error('Expected , or ) in function call');
}
}
this.currentToken++; // Skip )
return {
type: 'FunctionCall',
value: {
callee: identifier,
arguments: args
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseForStatement(): ASTNode {
this.currentToken++; // Skip FOR
const variable = this.parseIdentifier();
if (this.peek().value !== '=') {
throw new Error('Expected = in FOR loop initialization');
}
this.currentToken++;
const start = this.parseExpression();
if (this.peek().value.toUpperCase() !== 'TO') {
throw new Error('Expected TO in FOR loop');
}
this.currentToken++;
const end = this.parseExpression();
let step = null;
// Optional STEP
if (this.peek().value.toUpperCase() === 'STEP') {
this.currentToken++;
step = this.parseExpression();
}
const body = this.parseBlock();
// Must end with NEXT
if (this.peek().value.toUpperCase() !== 'NEXT') {
throw new Error('Expected NEXT at end of FOR loop');
}
this.currentToken++;
return {
type: 'ForStatement',
value: {
variable,
start,
end,
step,
body
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseForEachStatement(): ASTNode {
this.currentToken++; // Skip FOR
if (this.peek().value.toUpperCase() !== 'EACH') {
throw new Error('Expected EACH after FOR');
}
this.currentToken++;
const variable = this.parseIdentifier();
if (this.peek().value.toUpperCase() !== 'IN') {
throw new Error('Expected IN in FOR EACH statement');
}
this.currentToken++;
const collection = this.parseExpression();
const body = this.parseBlock();
// Must end with NEXT
if (this.peek().value.toUpperCase() !== 'NEXT') {
throw new Error('Expected NEXT at end of FOR EACH loop');
}
this.currentToken++;
return {
type: 'ForEachStatement',
value: {
variable,
collection,
body
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseFunctionStatement(): ASTNode {
this.currentToken++; // Skip FUNCTION
const name = this.parseIdentifier();
if (this.peek().value !== '(') {
throw new Error('Expected ( after function name');
}
this.currentToken++;
const params = this.parseParameterList();
if (this.peek().value !== ')') {
throw new Error('Expected ) after parameter list');
}
this.currentToken++;
const body = this.parseBlock();
// Must end with END FUNCTION
if (this.peek().value.toUpperCase() !== 'END' ||
this.tokens[this.currentToken + 1].value.toUpperCase() !== 'FUNCTION') {
throw new Error('Expected END FUNCTION');
}
this.currentToken += 2;
return {
type: 'FunctionDeclaration',
value: {
name,
params,
body
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseSelectStatement(): ASTNode {
this.currentToken++; // Skip SELECT
const fields = this.parseSelectList();
if (this.peek().value.toUpperCase() !== 'FROM') {
throw new Error('Expected FROM in SELECT statement');
}
this.currentToken++;
const tableName = this.parseIdentifier();
let whereClause = null;
// Optional WHERE clause
if (this.peek().value.toUpperCase() === 'WHERE') {
this.currentToken++;
whereClause = this.parseExpression();
}
return {
type: 'SelectStatement',
value: {
fields,
tableName,
whereClause
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseChartStatement(): ASTNode {
this.currentToken++; // Skip CHART
const type = this.parseExpression();
if (this.peek().value !== ',') {
throw new Error('Expected , after chart type');
}
this.currentToken++;
const data = this.parseExpression();
let legends = null;
let transpose = null;
let prompt = null;
// Check for additional parameters
while (this.peek().value === ',') {
this.currentToken++;
if (this.peek().value.toUpperCase() === 'LEGENDS') {
this.currentToken++;
legends = this.parseExpression();
} else if (this.peek().value.toUpperCase() === 'TRANSPOSE') {
this.currentToken++;
transpose = this.parseExpression();
} else if (this.peek().value.toUpperCase() === 'PROMPT') {
this.currentToken++;
prompt = this.parseExpression();
}
}
return {
type: 'ChartStatement',
value: {
type,
data,
legends,
transpose,
prompt
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseHearStatement(): ASTNode {
this.currentToken++; // Skip HEAR
const variable = this.parseIdentifier();
let kind = null;
let args = null;
// Check for AS clause
if (this.peek().value.toUpperCase() === 'AS') {
this.currentToken++;
kind = this.parseIdentifier();
// Optional arguments
if (this.peek().value === ',') {
this.currentToken++;
args = this.parseExpressionList();
}
}
return {
type: 'HearStatement',
value: {
variable,
kind,
args
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseTalkStatement(): ASTNode {
this.currentToken++; // Skip TALK
const text = this.parseExpression();
return {
type: 'TalkStatement',
value: text,
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
// Helper parsers
private parseIdentifier(): {value: string, line: number, column: number} {
const token = this.tokens[this.currentToken];
if (token.type !== 'WORD') {
throw new Error(`Expected identifier but got ${token.type}`);
}
this.currentToken++;
return {
value: token.value,
line: token.line,
column: token.column
};
}
private parseExpressionList(): ASTNode[] {
const expressions = [];
do {
expressions.push(this.parseExpression());
if (this.peek().value !== ',') {
break;
}
this.currentToken++;
} while (true);
return expressions;
}
private parseParameterList(): string[] {
const params = [];
while (this.peek().value !== ')') {
params.push(this.parseIdentifier().value);
if (this.peek().value !== ',') {
break;
}
this.currentToken++;
}
return params;
}
private parseSelectList(): string[] {
const fields = [];
do {
fields.push(this.parseIdentifier().value);
if (this.peek().value !== ',') {
break;
}
this.currentToken++;
} while (true);
return fields;
}
private parseTalkBlock(): ASTNode {
// Check if we're starting a talk block
if (!this.inTalkBlock && this.peek().value.toUpperCase() === 'BEGIN' &&
this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'TALK') {
this.currentToken += 2; // Skip BEGIN TALK
this.inTalkBlock = true;
this.talkBuffer = '';
return null;
}
// Check if we're ending a talk block
if (this.inTalkBlock && this.peek().value.toUpperCase() === 'END' &&
this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'TALK') {
this.currentToken += 2; // Skip END TALK
this.inTalkBlock = false;
return {
type: 'TalkBlock',
value: this.talkBuffer.trim(),
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
// If we're inside a talk block, accumulate text
if (this.inTalkBlock) {
const line = this.tokens[this.currentToken].value;
this.talkBuffer += line + '\n';
this.currentToken++;
return null;
}
throw new Error(
`Unexpected token in TALK block at line ${this.tokens[this.currentToken].line}`
);
}
private parseSystemPromptBlock(): ASTNode {
// Check if we're starting a system prompt block
if (!this.inSystemPrompt && this.peek().value.toUpperCase() === 'BEGIN' &&
this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'SYSTEM' &&
this.tokens[this.currentToken + 2]?.value.toUpperCase() === 'PROMPT') {
this.currentToken += 3; // Skip BEGIN SYSTEM PROMPT
this.inSystemPrompt = true;
this.systemPromptBuffer = '';
return null;
}
// Check if we're ending a system prompt block
if (this.inSystemPrompt && this.peek().value.toUpperCase() === 'END' &&
this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'SYSTEM' &&
this.tokens[this.currentToken + 2]?.value.toUpperCase() === 'PROMPT') {
this.currentToken += 3; // Skip END SYSTEM PROMPT
this.inSystemPrompt = false;
return {
type: 'SystemPromptBlock',
value: this.systemPromptBuffer.trim(),
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
// If we're inside a system prompt block, accumulate text
if (this.inSystemPrompt) {
const line = this.tokens[this.currentToken].value;
this.systemPromptBuffer += line + '\n';
this.currentToken++;
return null;
}
throw new Error(
`Unexpected token in SYSTEM PROMPT block at line ${this.tokens[this.currentToken].line}`
);
}
private parseTableDefinition(): ASTNode {
// Check if we're starting a table definition
if (!this.currentTable && this.peek().value.toUpperCase() === 'TABLE') {
this.currentToken++; // Skip TABLE
const tableName = this.parseIdentifier();
if (this.peek().value.toUpperCase() !== 'ON') {
throw new Error('Expected ON after table name');
}
this.currentToken++; // Skip ON
const connection = this.parseIdentifier();
this.currentTable = {
name: tableName.value,
connection: connection.value,
fields: {}
};
return null;
}
// Check if we're ending a table definition
if (this.currentTable && this.peek().value.toUpperCase() === 'END' &&
this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'TABLE') {
this.currentToken += 2; // Skip END TABLE
const table = this.currentTable;
this.currentTable = null;
return {
type: 'TableDefinition',
value: table,
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
// If we're inside a table definition, parse field definition
if (this.currentTable) {
const field = this.parseFieldDefinition();
if (field) {
this.currentTable.fields[field.name] = field.definition;
}
this.currentToken++;
return null;
}
throw new Error(
`Unexpected token in TABLE definition at line ${this.tokens[this.currentToken].line}`
);
}
private parseFieldDefinition(): { name: string; definition: any } | null {
const token = this.peek();
// Skip empty lines or comments
if (token.type === 'NEWLINE' ||
token.value.startsWith("'") ||
token.value.toUpperCase().startsWith('REM')) {
return null;
}
const name = this.parseIdentifier();
if (this.peek().value.toUpperCase() !== 'AS') {
throw new Error('Expected AS in field definition');
}
this.currentToken++; // Skip AS
const type = this.parseIdentifier();
let length = null;
let precision = null;
let scale = null;
// Check for size specification
if (this.peek().value === '(') {
this.currentToken++; // Skip (
length = this.parseNumber();
// Check for decimal precision
if (this.peek().value === ',') {
this.currentToken++; // Skip ,
precision = length;
scale = this.parseNumber();
length = null;
}
if (this.peek().value !== ')') {
throw new Error('Expected ) in field size specification');
}
this.currentToken++; // Skip )
}
return {
name: name.value,
definition: {
type: type.value,
length,
precision,
scale
}
};
}
private parseNumber(): number {
const token = this.peek();
if (token.type !== 'NUMBER') {
throw new Error('Expected number');
}
this.currentToken++;
return parseFloat(token.value);
}
private parseStatement(): ASTNode {
const token = this.tokens[this.currentToken];
// Skip empty lines
if (token.type === 'NEWLINE') {
this.currentToken++;
return null;
}
// Handle special blocks first
if (this.inTalkBlock) {
return this.parseTalkBlock();
}
if (this.inSystemPrompt) {
return this.parseSystemPromptBlock();
}
if (this.currentTable) {
return this.parseTableDefinition();
}
// Match tokens to specific statement types
switch (token.value.toUpperCase()) {
case 'INPUT':
return this.parseInputStatement();
case 'PRINT':
return this.parsePrintStatement();
case 'WRITE':
return this.parseWriteStatement();
case 'REM':
return this.parseRemStatement();
case 'CLOSE':
return this.parseCloseStatement();
case 'OPEN':
return this.parseOpenStatement();
case 'SELECT':
return this.parseSelectStatement();
case 'IF':
return this.parseIfStatement();
case 'FUNCTION':
return this.parseFunctionStatement();
case 'FOR':
return this.parseForStatement();
case 'DO':
return this.parseDoWhileStatement();
case 'WHILE':
return this.parseWhileStatement();
case 'TALK':
return this.parseTalkStatement();
case 'HEAR':
return this.parseHearStatement();
// Handle assignment statements
default:
if (this.isAssignment()) {
return this.parseAssignmentStatement();
}
throw new Error(`Unexpected token ${token.value} at line ${token.line}`);
}
}
private parseInputStatement(): ASTNode {
this.currentToken++; // Skip INPUT
const prompt = this.parseExpression();
let variable = null;
if (this.peek().value === ',') {
this.currentToken++; // Skip comma
variable = this.parseIdentifier();
}
return {
type: 'InputStatement',
value: {
prompt,
variable
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parsePrintStatement(): ASTNode {
this.currentToken++; // Skip PRINT
const expression = this.parseExpression();
let sessionName = null;
// Handle special #file printing
if (expression.value && expression.value.toString().startsWith('#')) {
sessionName = expression.value.substr(1, expression.value.indexOf(',') - 1);
const items = expression.value.substr(expression.value.indexOf(',') + 1)
.split(/[,;]/)
.map(item => item.trim());
return {
type: 'FilePrintStatement',
value: {
sessionName,
items
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return {
type: 'PrintStatement',
value: expression,
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseOpenStatement(): ASTNode {
this.currentToken++; // Skip OPEN
const filepath = this.parseExpression();
let mode = null;
let sessionName = null;
// Handle FOR APPEND/OUTPUT
if (this.peek().value.toUpperCase() === 'FOR') {
this.currentToken++;
mode = this.parseIdentifier().value;
}
// Handle AS #num or WITH #name
if (this.peek().value.toUpperCase() === 'AS' ||
this.peek().value.toUpperCase() === 'WITH') {
const kind = this.peek().value.toUpperCase();
this.currentToken += 2; // Skip AS/WITH and #
sessionName = this.parseExpression();
return {
type: 'OpenFileStatement',
value: {
filepath,
mode,
kind,
sessionName
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
return {
type: 'OpenStatement',
value: {
filepath,
mode
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseIfStatement(): ASTNode {
this.currentToken++; // Skip IF
const condition = this.parseCondition();
// Must have THEN
if (this.peek().value.toUpperCase() !== 'THEN') {
throw new Error('Expected THEN after IF condition');
}
this.currentToken++;
const thenBlock = this.parseBlock();
let elseBlock = null;
// Optional ELSE
if (this.peek().value.toUpperCase() === 'ELSE') {
this.currentToken++;
elseBlock = this.parseBlock();
}
// Must end with END IF
if (this.peek().value.toUpperCase() !== 'END' ||
this.tokens[this.currentToken + 1].value.toUpperCase() !== 'IF') {
throw new Error('Expected END IF');
}
this.currentToken += 2;
return {
type: 'IfStatement',
value: {
condition,
thenBlock,
elseBlock
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseCondition(): ASTNode {
const condition = this.parseExpression();
// Convert BASIC comparisons to JS
if (condition.type === 'BinaryExpression') {
switch (condition.value.operator) {
case '=':
condition.value.operator = '===';
break;
case '<>':
condition.value.operator = '!==';
break;
}
}
return condition;
}
private parseBlock(): ASTNode[] {
const statements: ASTNode[] = [];
while (this.currentToken < this.tokens.length) {
const token = this.peek();
if (['END', 'ELSE'].includes(token.value.toUpperCase())) {
break;
}
const statement = this.parseStatement();
if (statement) {
statements.push(statement);
}
}
return statements;
}
private generateIfCode(ast: ASTNode): void {
const { condition, thenBlock, elseBlock } = ast.value;
// Convert the condition and emit the if statement
const convertedCondition = this.generateCondition(condition);
this.emitCode(`if (${convertedCondition}) {`);
// Indent for then block
this.indent++;
// Generate code for the then block
thenBlock.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
// Handle optional else block
if (elseBlock) {
this.emitCode('} else {');
this.indent++;
elseBlock.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
}
this.emitCode('}');
}
private generateCondition(condition: ASTNode): string {
switch (condition.type) {
case 'BinaryExpression':
return this.generateBinaryCondition(condition);
case 'LogicalExpression':
return this.generateLogicalCondition(condition);
case 'UnaryExpression':
return this.generateUnaryCondition(condition);
case 'ParenthesizedExpression':
return `(${this.generateCondition(condition.value)})`;
case 'Identifier':
return condition.value;
case 'NumberLiteral':
case 'StringLiteral':
case 'BooleanLiteral':
return this.generateLiteral(condition);
case 'FunctionCall':
return this.generateFunctionCall(condition);
default:
throw new Error(`Unknown condition type: ${condition.type}`);
}
}
private generateBinaryCondition(condition: ASTNode): string {
const { operator, left, right } = condition.value;
// Convert BASIC operators to JavaScript
const jsOperator = this.convertOperator(operator);
return `${this.generateCondition(left)} ${jsOperator} ${this.generateCondition(right)}`;
}
private generateLogicalCondition(condition: ASTNode): string {
const { operator, left, right } = condition.value;
// Convert BASIC logical operators to JavaScript
const jsOperator = this.convertLogicalOperator(operator);
return `${this.generateCondition(left)} ${jsOperator} ${this.generateCondition(right)}`;
}
private generateUnaryCondition(condition: ASTNode): string {
const { operator, argument } = condition.value;
// Convert BASIC unary operators to JavaScript
const jsOperator = this.convertUnaryOperator(operator);
return `${jsOperator}${this.generateCondition(argument)}`;
}
private convertOperator(operator: string): string {
switch (operator.toUpperCase()) {
case '=': return '===';
case '<>': return '!==';
case '>=': return '>=';
case '<=': return '<=';
case '>': return '>';
case '<': return '<';
default: return operator;
}
}
private convertLogicalOperator(operator: string): string {
switch (operator.toUpperCase()) {
case 'AND': return '&&';
case 'OR': return '||';
case '&&': return '&&';
case '||': return '||';
default: return operator;
}
}
private convertUnaryOperator(operator: string): string {
switch (operator.toUpperCase()) {
case 'NOT': return '!';
case '!': return '!';
case '-': return '-';
default: return operator;
}
}
private generateLiteral(literal: ASTNode): string {
switch (literal.type) {
case 'NumberLiteral':
return literal.value.toString();
case 'StringLiteral':
return `"${literal.value}"`;
case 'BooleanLiteral':
return literal.value.toString();
default:
throw new Error(`Unknown literal type: ${literal.type}`);
}
}
private generateFunctionCall(call: ASTNode): string {
const { callee, arguments: args } = call.value;
const generatedArgs = args.map(arg => this.generateCondition(arg)).join(', ');
return `${callee.value}(${generatedArgs})`;
}
// Indentation handling
private indent: number = 0;
private getIndentation(): string {
return ' '.repeat(this.indent);
}
// Handle special BASIC condition functions
private isIntrinsicConditionFunction(name: string): boolean {
return [
'ISNULL',
'ISNUMERIC',
'ISDATE',
'ISEMPTY',
'CONTAINS',
'STARTSWITH',
'ENDSWITH'
].includes(name.toUpperCase());
}
private generateIntrinsicCondition(call: ASTNode): string {
const { callee, arguments: args } = call.value;
switch (callee.value.toUpperCase()) {
case 'ISNULL':
return `${this.generateCondition(args[0])} === null`;
case 'ISNUMERIC':
return `!isNaN(parseFloat(${this.generateCondition(args[0])}))`;
case 'ISDATE':
return `!isNaN(Date.parse(${this.generateCondition(args[0])}))`;
case 'ISEMPTY':
return `${this.generateCondition(args[0])} === ""`;
case 'CONTAINS':
return `${this.generateCondition(args[0])}.includes(${this.generateCondition(args[1])})`;
case 'STARTSWITH':
return `${this.generateCondition(args[0])}.startsWith(${this.generateCondition(args[1])})`;
case 'ENDSWITH':
return `${this.generateCondition(args[0])}.endsWith(${this.generateCondition(args[1])})`;
default:
throw new Error(`Unknown intrinsic function: ${callee.value}`);
}
}
private generateForCode(ast: ASTNode): void {
const { variable, start, end, step, body } = ast.value;
if (this.isForEach(ast)) {
this.generateForEachCode(ast);
return;
}
// Generate standard FOR loop initialization
const initValue = this.generateExpression(start);
const endValue = this.generateExpression(end);
const stepValue = step ? this.generateExpression(step) : '1';
const varName = this.generateExpression(variable);
// Generate the for loop with proper direction check
this.emitCode(`for (let ${varName} = ${initValue}; ` +
`${stepValue} > 0 ? ${varName} <= ${endValue} : ${varName} >= ${endValue}; ` +
`${varName} += ${stepValue}) {`);
// Indent and generate loop body
this.indent++;
body.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
this.emitCode('}');
}
private generateForEachCode(ast: ASTNode): void {
const { variable, collection, body } = ast.value;
// Special handling for collection with paging
if (this.hasPagedCollection(collection)) {
this.generatePagedForEachCode(ast);
return;
}
// Generate initialization code
this.emitCode(`
__totalCalls = 10;
__next = true;
__calls = 0;
__data = ${this.generateExpression(collection)};
__index = 0;
if (__data[0] && __data[0]['gbarray']) {
__data = __data.slice(1);
}
__pageMode = __data?.pageMode ? __data.pageMode : "none";
__url = __data?.links?.next?.uri;
__seekToken = __data.links?.self?.headers["MS-ContinuationToken"];
__totalCount = __data?.totalCount ? __data.totalCount : __data.length;
while (__next && __totalCount) {
let ${this.generateExpression(variable)} = __data?.items ?
__data?.items[__index] : __data[__index];
`);
// Indent and generate loop body
this.indent++;
body.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
// Generate paging logic
this.emitCode(`
__index = __index + 1;
if (__index === __totalCount) {
if (__calls < __totalCalls && __pageMode === "auto") {
let ___data = null;
await retry(async (bail) => {
await ensureTokens();
___data = await sys.getHttp({
pid: pid,
file: __url,
addressOrHeaders: headers,
httpUsername,
httpPs
});
}, { retries: 5 });
__data = ___data;
___data = null;
__url = __data?.links?.next?.uri;
__seekToken = __data?.links?.self?.headers["MS-ContinuationToken"];
__totalCount = __data?.totalCount ? __data.totalCount : __data.length;
__index = 0;
__calls++;
} else {
__next = false;
}
}
__data = null;
}`);
}
private generatePagedForEachCode(ast: ASTNode): void {
const { variable, collection, pageSize, body } = ast.value;
this.emitCode(`
if (!limit) limit = ${pageSize || 100};
__page = 1;
while (__page > 0 && __page < pages) {
let __res = null;
await retry(async (bail) => {
await ensureTokens();
__res = await sys.getHttp({
pid: pid,
file: host + ${this.generateExpression(collection.url)} +
'?' + pageVariable + '=' + __page +
'&' + limitVariable + '=' + limit,
addressOrHeaders: headers,
httpUsername,
httpPs
});
}, { retries: 5 });
await sleep(330);
res = __res;
__res = null;
list1 = res.data;
res = null;
let j1 = 0;
items1 = [];
while (j1 < ubound(list1)) {
let ${this.generateExpression(variable)} = list1[j1];
`);
// Indent and generate loop body
this.indent++;
body.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
// Generate paging footer
this.emitCode(`
j1 = j1 + 1;
}
list1 = null;
__page = list1?.length < limit ? 0 : __page + 1;
}`);
}
private isForEach(ast: ASTNode): boolean {
return ast.type === 'ForEachStatement';
}
private hasPagedCollection(collection: ASTNode): boolean {
return collection.type === 'PagedCollection';
}
private generateExpression(expr: ASTNode): string {
switch (expr.type) {
case 'Identifier':
return expr.value;
case 'NumberLiteral':
return expr.value.toString();
case 'StringLiteral':
return `"${expr.value}"`;
case 'BinaryExpression':
return this.generateBinaryExpression(expr);
case 'FunctionCall':
return this.generateFunctionCall(expr);
default:
throw new Error(`Unknown expression type: ${expr.type}`);
}
}
private generateBinaryExpression(expr: ASTNode): string {
const { operator, left, right } = expr.value;
return `${this.generateExpression(left)} ${operator} ${this.generateExpression(right)}`;
}
// Special handling for collection operations
private generateCollectionOperation(operation: ASTNode): string {
const { method, args } = operation.value;
switch (method) {
case 'FILTER':
return `filter(${args.map(arg => this.generateExpression(arg)).join(', ')})`;
case 'MAP':
return `map(${args.map(arg => this.generateExpression(arg)).join(', ')})`;
case 'REDUCE':
return `reduce(${args.map(arg => this.generateExpression(arg)).join(', ')})`;
default:
throw new Error(`Unknown collection operation: ${method}`);
}
}
// Loop control statement handling
private generateContinue(): void {
this.emitCode('continue;');
}
private generateBreak(): void {
this.emitCode('break;');
}
private generateExit(): void {
this.emitCode('break;');
}
private generateCode(ast: ASTNode): void {
switch (ast.type) {
case 'Program':
ast.children?.forEach(child => this.generateCode(child));
break;
case 'InputStatement':
this.generateInputCode(ast);
break;
case 'PrintStatement':
this.generatePrintCode(ast);
break;
case 'FilePrintStatement':
this.generateFilePrintCode(ast);
break;
case 'OpenStatement':
this.generateOpenCode(ast);
break;
case 'IfStatement':
this.generateIfCode(ast);
break;
case 'ForStatement':
this.generateForCode(ast);
break;
case 'WhileStatement':
this.generateWhileCode(ast);
break;
case 'ForEachStatement':
this.generateForEachCode(ast);
break;
case 'HearStatement':
this.generateHearCode(ast);
break;
case 'TalkStatement':
this.generateTalkCode(ast);
break;
case 'AssignmentStatement':
this.generateAssignmentCode(ast);
break;
}
}
private generateInputCode(ast: ASTNode): void {
const {prompt, variable} = ast.value;
if (variable) {
this.emitCode(`
TALK ${this.generateExpression(prompt)}
HEAR ${this.generateExpression(variable)}
`);
} else {
this.emitCode(`HEAR ${this.generateExpression(prompt)}`);
}
}
private generatePrintCode(ast: ASTNode): void {
this.emitCode(`await dk.talk({pid: pid, text: ${this.generateExpression(ast.value)}})`);
}
private generateFilePrintCode(ast: ASTNode): void {
const {sessionName, items} = ast.value;
if (items.length > 1) {
this.emitCode(
`await sys.save({pid: pid, file: files[${sessionName}], args:[${items.join(',')}]})`
);
} else {
this.emitCode(
`await sys.set({pid: pid, file: files[${sessionName}], address: col++, name: "${items[0]}", value: ${items[0]}})`
);
}
}
private parseDoWhileStatement(): ASTNode {
this.currentToken++; // Skip DO
if (this.peek().value.toUpperCase() !== 'WHILE') {
throw new Error(`Expected WHILE after DO at line ${this.line}`);
}
this.currentToken++; // Skip WHILE
const condition = this.parseCondition();
const body = this.parseBlock();
// Must end with LOOP
if (this.peek().value.toUpperCase() !== 'LOOP') {
throw new Error(`Expected LOOP at line ${this.line}`);
}
this.currentToken++; // Skip LOOP
return {
type: 'WhileStatement',
value: {
condition,
body
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private isAssignment(): boolean {
// Look ahead to check for assignment pattern
let i = this.currentToken;
// Skip identifier(s)
while (i < this.tokens.length &&
(this.tokens[i].type === 'WORD' ||
this.tokens[i].value === '.' ||
this.tokens[i].value === '[' ||
this.tokens[i].value === ']')) {
i++;
}
// Check if next non-whitespace token is =
while (i < this.tokens.length) {
if (this.tokens[i].type !== 'WHITESPACE') {
return this.tokens[i].value === '=';
}
i++;
}
return false;
}
private generateWhileCode(ast: ASTNode): void {
const { condition, body } = ast.value;
// Convert the condition
const convertedCondition = this.generateCondition(condition);
// Emit while loop
this.emitCode(`while (${convertedCondition}) {`);
// Indent and generate loop body
this.indent++;
body.forEach(statement => {
this.generateCode(statement);
});
this.indent--;
this.emitCode('}');
}
private parseAssignmentStatement(): ASTNode {
let left = this.parseLeftHandSide();
if (this.peek().value !== '=') {
throw new Error(`Expected = but got ${this.peek().value} at line ${this.line}`);
}
this.currentToken++; // Skip =
// Handle special assignments
const nextToken = this.peek();
let right;
switch (nextToken.value.toUpperCase()) {
case 'SELECT':
right = this.parseSelectStatement();
break;
case 'CHART':
right = this.parseChartStatement();
break;
case 'GET':
right = this.parseGetStatement();
break;
case 'POST':
right = this.parseHttpStatement('POST');
break;
case 'PUT':
right = this.parseHttpStatement('PUT');
break;
case 'BLUR':
right = this.parseImageOperation('BLUR');
break;
case 'SHARPEN':
right = this.parseImageOperation('SHARPEN');
break;
case 'FORMAT':
right = this.parseFormatStatement();
break;
case 'DATEDIFF':
right = this.parseDateOperation('DATEDIFF');
break;
case 'DATEADD':
right = this.parseDateOperation('DATEADD');
break;
case 'NEW':
right = this.parseNewStatement();
break;
case 'FIND':
right = this.parseFindStatement();
break;
case 'CREATE':
if (this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'DEAL') {
right = this.parseCreateDealStatement();
} else {
right = this.parseCreateStatement();
}
break;
case 'ACTIVE':
if (this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'TASKS') {
right = this.parseActiveTasksStatement();
} else {
right = this.parseExpression();
}
break;
case 'UPLOAD':
right = this.parseUploadStatement();
break;
case 'FILL':
right = this.parseFillStatement();
break;
case 'CARD':
right = this.parseCardStatement();
break;
case 'ALLOW':
if (this.tokens[this.currentToken + 1]?.value.toUpperCase() === 'ROLE') {
right = this.parseAllowRoleStatement();
} else {
right = this.parseExpression();
}
break;
default:
right = this.parseExpression();
}
return {
type: 'AssignmentStatement',
value: {
left,
right,
operator: '='
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseLeftHandSide(): ASTNode {
let identifier = this.parseIdentifier();
// Handle property access (obj.prop) and array access (arr[index])
while (
this.peek().value === '.' ||
this.peek().value === '[') {
if (this.peek().value === '.') {
this.currentToken++; // Skip .
const property = this.parseIdentifier();
identifier = {
type: 'MemberExpression',
value: {
object: identifier,
property,
computed: false
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
} else {
this.currentToken++; // Skip [
const index = this.parseExpression();
if (this.peek().value !== ']') {
throw new Error(`Expected ] at line ${this.line}`);
}
this.currentToken++; // Skip ]
identifier = {
type: 'MemberExpression',
value: {
object: identifier,
property: index,
computed: true
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
}
return identifier;
}
// Helper method to handle async operations in assignments
private isAsyncOperation(rightType: string): boolean {
const asyncOperations = [
'SelectStatement',
'HttpRequest',
'ImageOperation',
'ChartStatement',
'FindStatement',
'UploadStatement',
'CreateDealStatement',
'ActiveTasksStatement',
'AllowRoleStatement',
'FillStatement',
'CardStatement'
];
return asyncOperations.includes(rightType);
}
private generateAssignmentCode(ast: ASTNode): void {
const { left, right, operator } = ast.value;
const leftCode = this.generateExpression(left);
// Check if this is an async operation
if (this.isAsyncOperation(right.type)) {
this.emitCode(`${leftCode} = await ${this.generateAsyncOperation(right)};`);
} else {
this.emitCode(`${leftCode} = ${this.generateExpression(right)};`);
}
}
private generateAsyncOperation(operation: ASTNode): string {
switch (operation.type) {
case 'SelectStatement':
return this.generateSelectOperation(operation);
case 'HttpRequest':
return this.generateHttpOperation(operation);
case 'ImageOperation':
return this.generateImageOperation(operation);
case 'ChartStatement':
return this.generateChartOperation(operation);
// ... handle other async operations
default:
throw new Error(`Unknown async operation type: ${operation.type}`);
}
}
private generateOpenCode(ast: ASTNode): void {
let {filepath, mode, kind, sessionName} = ast.value;
if (kind === 'AS' && this.isNumber(sessionName)) {
const filename = `${filepath.substr(0, filepath.lastIndexOf('.'))}.xlsx`;
this.emitCode(`
col = 1
await sys.save({pid: pid, file: "${filename}", args: [id]})
await dk.setFilter({pid: pid, value: "id=" + id})
files[${sessionName}] = "${filename}"
`);
} else {
const params = this.generateParams(['url', 'username', 'password'], [filepath]);
sessionName = sessionName ? `"${sessionName}"` : null;
const kindStr = `"${kind}"`;
this.emitCode(
`page = await wa.openPage({pid: pid, handle: page, sessionKind: ${kindStr}, sessionName: ${sessionName}, ${params}})`
);
}
}
private generateHearCode(ast: ASTNode): void {
const {variable, kind, args} = ast.value;
if (kind) {
if (kind === 'sheet') {
this.emitCode(
`${variable} = await dk.hear({pid: pid, kind:"sheet", arg: "${args[0]}"})`
);
} else {
this.emitCode(
`${variable} = await dk.hear({pid: pid, kind:"${kind}"${args ? `, args: [${args}]` : ''}})`
);
}
} else {
this.emitCode(`${variable} = await dk.hear({pid: pid})`);
}
}
private generateTalkCode(ast: ASTNode): void {
const text = this.normalizeQuotes(ast.value);
this.emitCode(`await dk.talk({pid: pid, text: ${text}})`);
}
private generateChartAssignment(left: string, right: ASTNode): void {
const {type, data, legends, transpose, prompt} = right.value;
// Handle regular chart
if (!prompt) {
this.emitCode(`
${left} = await dk.chart({
pid: pid,
type: ${type},
data: ${data},
legends: ${legends},
transpose: ${transpose}
})`
);
}
// Handle chart with prompt (LLM chart)
else {
this.emitCode(`
${left} = await dk.llmChart({
pid: pid,
type: ${type},
data: ${data},
prompt: ${prompt}
})`
);
}
}
private generateSelectAssignment(left: string, right: ASTNode): void {
const {tableName, sql} = right.value;
// Replace table name with ? in SQL
const sqlWithPlaceholder = sql.replace(tableName, '?');
this.emitCode(`
${left} = await sys.executeSQL({
pid: pid,
data: ${tableName},
sql: \`${sqlWithPlaceholder}\`
})`
);
}
private generateHttpAssignment(left: string, right: ASTNode): void {
const {method, url, data} = right.value;
switch (method.toUpperCase()) {
case 'GET':
this.emitCode(`
if (${url}.endsWith('.pdf') && !${url}.startsWith('https')) {
${left} = await sys.getPdf({pid: pid, file: ${url}});
} else {
let __${left} = null;
await retry(async (bail) => {
await ensureTokens();
__${left} = await sys.getHttp({
pid: pid,
file: ${url},
addressOrHeaders: headers,
httpUsername,
httpPs
});
}, { retries: 5 });
${left} = __${left};
__${left} = null;
}
`);
break;
case 'POST':
this.emitCode(`
await retry(async (bail) => {
await ensureTokens();
__${left} = await sys.postByHttp({
pid: pid,
url: ${url},
data: ${data},
headers
});
}, { retries: 5 });
${left} = __${left};
`);
break;
case 'PUT':
this.emitCode(`
await retry(async (bail) => {
await ensureTokens();
__${left} = await sys.putByHttp({
pid: pid,
url: ${url},
data: ${data},
headers
});
}, { retries: 5 });
${left} = __${left};
`);
break;
}
}
private generateImageAssignment(left: string, right: ASTNode): void {
const {operation, args} = right.value;
switch (operation) {
case 'BLUR':
this.emitCode(`
${left} = await img.blur({
pid: pid,
args: [${args.join(',')}]
})`
);
break;
case 'SHARPEN':
this.emitCode(`
${left} = await img.sharpen({
pid: pid,
args: [${args.join(',')}]
})`
);
break;
case 'GET IMAGE':
this.emitCode(`
${left} = await img.getImageFromPrompt({
pid: pid,
prompt: ${args[0]}
})`
);
break;
}
}
private generateDateAssignment(left: string, right: ASTNode): void {
const {operation, params} = right.value;
switch (operation) {
case 'DATEDIFF':
this.emitCode(`
${left} = await dk.getDateDiff({
pid: pid,
date1: ${params.date1},
date2: ${params.date2},
mode: ${params.mode}
})`
);
break;
case 'DATEADD':
this.emitCode(`
${left} = await dk.dateAdd({
pid: pid,
date: ${params.date},
mode: ${params.mode},
units: ${params.units}
})`
);
break;
}
}
private parseWriteStatement(): ASTNode {
this.currentToken++; // Skip WRITE
// Parse the expression to be written
const expression = this.parseExpression();
// WRITE is syntactic sugar for PRINT in BASIC
return {
type: 'PrintStatement', // We reuse PrintStatement since WRITE converts to PRINT
value: expression,
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseRemStatement(): ASTNode {
this.currentToken++; // Skip REM
// Collect all tokens until end of line as comment text
let commentText = '';
while (this.currentToken < this.tokens.length &&
this.tokens[this.currentToken].type !== 'NEWLINE') {
commentText += this.tokens[this.currentToken].value + ' ';
this.currentToken++;
}
return {
type: 'CommentStatement',
value: commentText.trim(),
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private parseCloseStatement(): ASTNode {
this.currentToken++; // Skip CLOSE
// Optional file number or identifier
let fileRef = null;
if (this.peek().type !== 'NEWLINE') {
fileRef = this.parseExpression();
}
return {
type: 'CloseStatement',
value: fileRef,
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private generateCloseCode(ast: ASTNode): void {
// CLOSE is typically a no-op in the modern context
// but we might want to handle cleanup of file handles
if (ast.value) {
this.emitCode(`// Closing file ${ast.value}`);
}
}
private generateWriteCode(ast: ASTNode): void {
// WRITE statement gets converted to PRINT
this.emitCode(`PRINT${this.generateExpression(ast.value)}`);
}
private generateRemCode(ast: ASTNode): void {
// Comments are preserved but as JS-style comments
this.emitCode(`// ${ast.value}`);
}
// Helper method to handle file operations
private isFileOperation(token: Token): boolean {
return token.value.startsWith('#') ||
this.peek(1)?.value === '#' ||
this.isFileHandle(token);
}
private isFileHandle(token: Token): boolean {
// Check if token represents a file handle
return token.type === 'WORD' &&
this.fileHandles.has(token.value);
}
// Additional helper for parsing file specifications
private parseFileSpec(): {
handle: string;
mode?: string;
access?: string;
} {
let handle: string;
let mode: string;
let access: string;
if (this.peek().value === '#') {
this.currentToken++; // Skip #
handle = this.parseExpression().value;
} else {
handle = this.parseExpression().value;
}
// Check for optional mode (FOR INPUT/OUTPUT/APPEND)
if (this.peek().value.toUpperCase() === 'FOR') {
this.currentToken++; // Skip FOR
mode = this.peek().value.toUpperCase();
this.currentToken++; // Skip mode
}
// Check for optional access (SHARED/LOCK READ/LOCK WRITE)
if (this.peek().value.toUpperCase() === 'ACCESS') {
this.currentToken++; // Skip ACCESS
access = this.peek().value.toUpperCase();
this.currentToken++; // Skip access mode
}
return { handle, mode, access };
}
// Helper for tracking file handles
private fileHandles = new Set<string>();
private registerFileHandle(handle: string): void {
this.fileHandles.add(handle);
}
private unregisterFileHandle(handle: string): void {
this.fileHandles.delete(handle);
}
// Extended parseStatement to handle all file operations
private parseFileStatement(): ASTNode {
const operation = this.peek().value.toUpperCase();
this.currentToken++; // Skip operation keyword
switch (operation) {
case 'WRITE':
return this.parseWriteStatement();
case 'PRINT':
if (this.isFileOperation(this.peek())) {
return this.parseFilePrintStatement();
}
return this.parsePrintStatement();
case 'CLOSE':
return this.parseCloseStatement();
case 'OPEN':
return this.parseOpenStatement();
default:
throw new Error(
`Unknown file operation ${operation} at line ${this.tokens[this.currentToken].line}`
);
}
}
private parseFilePrintStatement(): ASTNode {
const fileSpec = this.parseFileSpec();
if (this.peek().value !== ',') {
throw new Error('Expected , after file specification');
}
this.currentToken++; // Skip ,
const expressions: ASTNode[] = [];
do {
expressions.push(this.parseExpression());
if (this.peek().value !== ',') {
break;
}
this.currentToken++; // Skip ,
} while (true);
return {
type: 'FilePrintStatement',
value: {
file: fileSpec,
expressions
},
line: this.tokens[this.currentToken].line,
column: this.tokens[this.currentToken].column
};
}
private generateFormatAssignment(left: string, right: ASTNode): void {
const {value, format} = right.value;
this.emitCode(`
${left} = await dk.format({
pid: pid,
value: ${value},
format: ${format}
})`
);
}
private generateCardAssignment(left: string, right: ASTNode): void {
const {doc, data} = right.value;
this.emitCode(`
${left} = await dk.card({
pid: pid,
args: [${doc}, ${data}]
})`
);
}
private generateAssignmentCode(ast: ASTNode): void {
const {left, right} = ast.value;
switch (right.type) {
case 'SelectStatement':
this.generateSelectAssignment(left, right);
break;
case 'ChartStatement':
this.generateChartAssignment(left, right);
break;
case 'HttpRequest':
this.generateHttpAssignment(left, right);
break;
default:
this.emitCode(`${left} = ${this.generateExpression(right)}`);
}
}
// Utility methods
private emitCode(code: string): void {
this.output.push(code);
this.lineMap[this.output.length] = this.line;
}
private isNumber(value: any): boolean {
return !isNaN(parseFloat(value)) && isFinite(value);
}
private normalizeQuotes(text: string): string {
if (!text.trim().startsWith('`') && !text.trim().startsWith("'")) {
return '`' + text + '`';
}
return text;
}
private generateParams(names: string[], values: any[]): string {
let params = '';
names.forEach((name, i) => {
const value = values[i];
params += `"${name}": ${value === undefined ? null : value}${i < names.length - 1 ? ', ' : ''}`;
});
return params;
}
private peek(offset: number = 0): Token {
return this.tokens[this.currentToken + offset];
}
private generateMetadata(mainName: string): any {
return {
name: mainName,
description: this.systemPromptBuffer || '',
properties: []
};
}
// Helper method to split params but ignore commas in quotes (from KeywordsExpressions)
private splitParams(str: string): string[] {
return str.split(',').reduce(
(accum: {soFar: string[], isConcatting: boolean}, curr: string) => {
if (accum.isConcatting) {
accum.soFar[accum.soFar.length - 1] += ',' + curr;
} else {
accum.soFar.push(curr ? curr.trim() : '');
}
if (curr.split('`').length % 2 === 0) {
accum.isConcatting = !accum.isConcatting;
}
return accum;
},
{ soFar: [], isConcatting: false }
).soFar;
}
// Converts BASIC conditions to JS conditions
private convertConditions(input: string): string {
let result = input.replace(/ +and +/gi, ' && ');
result = result.replace(/ +or +/gi, ' || ');
result = result.replace(/ +not +/gi, ' !');
result = result.replace(/ +<> +/gi, ' !== ');
result = result.replace(/ += +/gi, ' === ');
return result;
}
}