2550 lines
65 KiB
Text
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;
|
|
}
|
|
}
|