UNPKG

tcl-js

Version:

tcl-js is a tcl intepreter written completely in Typescript. It is meant to replicate the tcl-sh interpreter as closely as possible.

969 lines (842 loc) 28.4 kB
import { Program, Parser, CommandToken, ArgToken } from './parser'; import { Scope } from './scope'; import { Tcl } from './tcl'; import { TclSimple, TclVariable, TclObject, TclArray, TclList, TclProcHelpers, TclProc, ProcArgs, } from './types'; import { TclError } from './tclerror'; import { Parser as MathParser } from './math/parser.js'; /** * Executes a tcl program * * @export * @class Interpreter */ export class Interpreter { private program: Program; private scope: Scope; private lastValue: TclVariable = new TclSimple(''); private tcl: Tcl; /** * Creates an instance of Interpreter. * * @param {Tcl} tcl - The parent Tcl for keeping certain variables * @param {string} input - The input code you want to interpret * @param {Scope} scope - The scope you want to use * @memberof Interpreter */ public constructor(tcl: Tcl, input: string, scope: Scope) { let parser = new Parser(input); this.program = parser.get(); this.scope = scope; this.tcl = tcl; } /** * Actually runs the code * * @returns {Promise<TclVariable>} - Value of the last command ran * @memberof Interpreter */ public async run(): Promise<TclVariable> { for (let command of this.program.commands) { this.lastValue = await this.processCommand(command); // Check if it should stop running commands when hitting a break or continue let checkLoop = this.scope.getSetting('loop'); if (checkLoop && typeof checkLoop !== 'boolean') { if (checkLoop.break || checkLoop.continue) break; } } return this.lastValue; } /** * Getter for the scope * * @returns {Scope} * @memberof Interpreter */ public getScope(): Scope { return this.scope; } /** * Getter for the parent Tcl * * @returns {Tcl} * @memberof Interpreter */ public getTcl(): Tcl { return this.tcl; } /** * This will reset the state of the interpreter, but keep the processed code * * @param {Scope} scope * @memberof Interpreter */ public reset(scope?: Scope) { if (scope) this.scope = scope; this.lastValue = new TclSimple(''); } /** * Internal function to process commands * * @private * @param {CommandToken} command - Command to process * @returns {Promise<TclVariable>} - Processed result * @memberof Interpreter */ private async processCommand(command: CommandToken): Promise<TclVariable> { // Map the args from wordtokens to tclvariables let args: ProcArgs = []; for (let i = 0; i < command.args.length; i++) { let processed = await this.processArg(command.args[i]); if (command.args[i].expand) { let list = (<TclSimple>processed).getList(); for (let j = 0; j < list.getLength(); j++) { let item = list.getSubValue(j); (<TclVariable[]>args).push(item); } } else { (<TclVariable[]>args).push(processed); } } // Return the result of the associated function being called let proc = this.scope.resolveProc(command.command); // First check if function exists if (!proc) throw new TclError(`invalid command name "${command.command}"`); let options = (<TclProc>proc).options; // Setup helper functions let helpers: TclProcHelpers = { sendHelp: (helpType) => { let message = options.helpMessages[helpType] || 'Error'; if (options.arguments.pattern) message += `: should be "${options.arguments.pattern}"`; // Throw an advanced error throw new TclError( `${message}\n while reading: "${command.source}"\n at line #${ command.sourceLocation }\n`, ); }, solveExpression: async (expression) => { // Process the subexpressions and variables let processedExpression = await this.deepProcess(expression); // Check if it did not result in a string if (typeof processedExpression !== 'string') { // If so convert if possible, otherwise throw error if (processedExpression instanceof TclSimple) processedExpression = processedExpression.getValue(); else throw new TclError('expression resolved to unusable value'); } let parser = new MathParser(); // Try to solve the expression and return the result let solvedExpression = parser.parse(processedExpression).evaluate(); //Check if the result is usable if (typeof solvedExpression === 'string') solvedExpression = parseFloat(solvedExpression); if (typeof solvedExpression === 'boolean') solvedExpression = solvedExpression ? 1 : 0; if (typeof solvedExpression !== 'number' || isNaN(solvedExpression)) throw new TclError('expression resolved to unusable value'); if (solvedExpression === Infinity) throw new TclError('expression result is infinity'); return solvedExpression; }, }; // Check if the amount of arguments is correct // Set to -1 for infinite if (typeof options.arguments.amount === 'number') { if ( args.length !== options.arguments.amount && options.arguments.amount !== -1 ) return helpers.sendHelp('wargs'); } else { if ( (args.length < options.arguments.amount.start && options.arguments.amount.start !== -1) || (args.length > options.arguments.amount.end && options.arguments.amount.end !== -1) ) return helpers.sendHelp('wargs'); } if (options.arguments.textOnly || options.arguments.simpleOnly) { // Check if arguments are correct if simpleonly for (let arg of args) { if (!(arg instanceof TclSimple)) return helpers.sendHelp('wtype'); } } if (options.arguments.textOnly === true) { // Create a full expression by joining all arguments args = (<TclSimple[]>args).map((arg) => arg.getValue()); } // Call the function return proc.callback(this, args, command, helpers); } /** * Processes arguments * * @private * @param {ArgToken} arg * @returns {Promise<TclVariable>} * @memberof Interpreter */ private async processArg(arg: ArgToken): Promise<TclVariable> { // Define an output to return let output: string | TclVariable = arg.value; // Check if the lexer allows solving if (arg.hasVariable && arg.hasVariable && typeof output === 'string') { // If so, resolve those output = await this.deepProcess(output); } // If the lexer has not determined to stop backslash processing, process all the backslashes if (!arg.stopBackslash && typeof output === 'string') output = this.processBackSlash(output); // Always replace escaped endlines if (typeof output === 'string') output = output.replace(/\\\n/g, ' '); // Return a new TclSimple with the previously set output return typeof output === 'string' ? new TclSimple(output) : output; } /** * Function to go over a string and solve expressions and variables accordingly * * @param {string} input - The string to go over * @param {number} [position=0] - At what point to start in the string * @returns {(Promise<TclVariable | string>)} - The found results * @memberof Interpreter */ public async deepProcess( input: string, position: number = 0, ): Promise<TclVariable | string> { // Initialize output string let output = ''; // Intitialize variable for every found variable let toProcess: FoundVariable | null; // Keep going as long as there are variables while ((toProcess = await this.resolveFirst(input, position))) { // Add the string until the first found variable while (position < toProcess.startPosition) { output += input.charAt(position); position++; } // Jump to the end of the variable position = toProcess.endPosition; // Return the full value if it corrresponds to the entire string if (toProcess.raw === input) return toProcess.value; // Otherwise add the result to the output output += toProcess.value.getValue(); } // Add the last bit of the string while (position < input.length) { output += input.charAt(position); position++; } return output; } /** * Function to return the first resolved variable or subexpression * * @private * @param {string} input - What string to read for those expressions * @param {number} [position] - Where in the string to start searching * @returns {(Promise<FoundVariable | null>)} - The found result * @memberof Interpreter */ private async resolveFirst( input: string, position: number, ): Promise<FoundVariable | null> { // Setup necessary variables let char = input.charAt(position); // Function to progress one char function read() { position += 1; char = input.charAt(position); } // Keep reading string until a something is found while (char !== '[' && char !== '$' && position < input.length) { if (char === '\\') read(); read(); } // Return the correct fix according to the found char if (char === '[') { return this.resolveFirstSquareBracket(input, position); } else if (char === '$') { return this.resolveFirstVariable(input, position); } else { return null; } } // Not necessary /** * Function to loop over every variable and solve all of them in order * * @param {string} input - The string to loop over * @param {number=0} position - At what position in the string to start * @returns Promise - The solved result */ /* public async deepProcessVariables( input: string, position: number = 0, ): Promise<TclVariable | string> { // Initialize output string let output = ''; // Intitialize variable for every found variable let toProcess: FoundVariable | null; // Keep going as long as there are variables while ((toProcess = await this.resolveFirstVariable(input, position))) { // Add the string until the first found variable while (position < toProcess.startPosition) { output += input.charAt(position); position++; } // Jump to the end of the variable position = toProcess.endPosition; // Return the full value if it corrresponds to the entire string if (toProcess.raw === input) return toProcess.value; // Otherwise add the result to the output output += toProcess.value.getValue(); } // Add the last bit of the string while (position < input.length) { output += input.charAt(position); position++; } return output; } */ /** * Function to read a string until the first found variable * It will then solve the variable and return that * * @private * @param {string} input - The string that will be searched for variables * @param {number} [position=0] - The position to start searching at * @returns {(Promise<FoundVariable | null>)} - The found results * @memberof Interpreter */ private async resolveFirstVariable( input: string, position: number = 0, ): Promise<FoundVariable | null> { // Setup necessary variables let char = input.charAt(position); // Setup an output buffer let currentVar = { // Exmample: name(bracket) name: '', bracket: '', // Originalstring: $name(bracket) originalString: '', // If the variable is curly: ${curly(variable)} curly: false, }; // Keep track if we are in a bracket () or not let inBracket = false; /** * Function to progress one char * * @param {boolean} appendOnOriginal */ function read(appendOnOriginal: boolean) { if (appendOnOriginal) currentVar.originalString += char; position += 1; char = input.charAt(position); } // Keep reading string until a $ is found while (char !== '$' && position < input.length) { if (char === '\\') read(false); read(false); } // Check if it was not the end of the string that stopped us if (char !== '$') return null; char = <string>char; // Record the startposition of the variable let startPosition = position; // Eat the $ character read(true); // Check if the variable is curly if (char === '{') { currentVar.curly = true; read(true); } // While input is abvailable while (position < input.length) { // Check if we are within () if (inBracket) { if (char === ')') { inBracket = false; read(true); break; } // Solve any found variables if (char === '$') { let replaceVar = await this.resolveFirstVariable(input, position); if (replaceVar) { while (position < replaceVar.endPosition) { read(true); } currentVar.bracket += replaceVar.value.getValue(); continue; } } // Solve any found subexpressions if (char === '[') { let replaceVar = await this.resolveFirstSquareBracket( input, position, ); if (replaceVar) { while (position < replaceVar.endPosition) { read(true); } currentVar.bracket += replaceVar.value.getValue(); continue; } } // If not } else { // Check if we should enter brackets if (char === '(') { inBracket = true; // Eat the ( character read(true); continue; } // If not check if the character is normal wordcharacter // Although this is only needed when we are not within {} if (!currentVar.curly && !char.match(/\w/g)) break; } // If the variable is curly and we hit an end }, break; if (currentVar.curly && char === '}') break; // Check for escape chars if (char === '\\') { if (currentVar.curly) { if (inBracket) currentVar.bracket += char; else currentVar.name += char; } read(true); } // Add the character to the corresponding string if (inBracket) currentVar.bracket += char; else currentVar.name += char; // Next char read(true); } // If the variable is curly check if we ended on an } if (currentVar.curly) { if (char !== '}') throw new TclError('unexpected end of string'); // Eat the } read(true); } // Check if we did not just hit a lonesome $, if we did return the next variable if (currentVar.name === '') return this.resolveFirstVariable(input, position); // Initialize an index let index: string | number | null; let solved = currentVar.bracket; // Set the correct key if (solved === '') index = null; else if (isNumber(solved)) index = parseInt(solved, 10); else index = solved; // And return the solved variable with extra info return { raw: currentVar.originalString, startPosition, endPosition: position, value: this.getVariable(currentVar.name, index), }; } /** * Grabs a variable with full name parsing: so "name" "name(obj)" and "name(3)" will all work * * @param {string} variableName - The advanced variable name * @param {(string | number | null)} variableKey - If necessary, the array or object key in the variable * @returns {TclVariable} - The resolved variable * @memberof Interpreter */ public getVariable( variableName: string, variableKey: string | number | null, ): TclVariable { // Set the correct keys let name = variableName; let objectKey = typeof variableKey === 'string' ? variableKey : null; let arrayIndex = typeof variableKey === 'number' ? variableKey : null; // Get the corresponding value let value: TclVariable | null = this.scope.resolve(name); if (!value) throw new TclError(`can't read "${name}": no such variable`); // Check if an object key is present if (objectKey !== null) { // Check if the value is indeed an object if (!(value instanceof TclObject)) throw new TclError(`can't read "${name}": variable isn't object`); // Return the value at the given key return value.getSubValue(objectKey); } // Check if an array index is present else if (arrayIndex !== null) { // Check if the value is indeed an array if (!(value instanceof TclArray)) throw new TclError(`can't read "${name}": variable isn't array`); // Return the value at the given index return value.getSubValue(arrayIndex); } // If none are present, just return the value else { return value; } } /** * Sets a variable with full name parsing * * @param {string} variableName - The advanced variable name * @param {(string | number | null)} variableKey - If necessary, the array or object key in the variable * @param {TclVariable} variable - The variable to put at that index * @returns * @memberof Interpreter */ public setVariable( variableName: string, variableKey: string | number | null, variable: TclVariable, ) { // Set the correct keys let name = variableName; let objectKey = typeof variableKey === 'string' ? variableKey : null; let arrayIndex = typeof variableKey === 'number' ? variableKey : null; // Define an output let output: TclVariable = variable; // Read if the value is already present in the scope let existingValue: TclVariable | null = this.scope.resolve(name); // Check if an object key was parsed if (objectKey !== null) { if (existingValue) { // If a value is already present, check if it is indeed an object if (!(existingValue instanceof TclObject)) throw new TclError( `cant' set "${variableName}": variable isn't object`, ); // Update the object with the value and return existingValue.set(objectKey, variable); return; } // Create a new object, add the value let obj = new TclObject(undefined, name); obj.set(objectKey, variable); // Put the new object in the output output = obj; } // Check an array index was parsed else if (arrayIndex !== null) { if (existingValue) { // If a value is already present, check if it is indeed an array if (!(existingValue instanceof TclArray)) throw new TclError( `cant' set "${variableName}": variable isn't array`, ); // Update the array with the value and return existingValue.set(arrayIndex, variable); return; } // Create a new array, add the value let arr = new TclArray(undefined, name); arr.set(arrayIndex, variable); // Put the new array in the output output = arr; } // It is just a normal object else { // Check if the existingValue is not already a different object if (existingValue instanceof TclObject) throw new TclError(`cant' set "${variableName}": variable is object`); if (existingValue instanceof TclArray) throw new TclError(`cant' set "${variableName}": variable is array`); if (existingValue instanceof TclList) throw new TclError(`cant' set "${variableName}": variable is list`); } // Set the name output.setName(name); // Set the scope correctly this.scope.define(name, output); return; } /** * Delete a variable * * @param {string} variableName - The name of the variable * @param {(string | number | null)} variableKey - If to delete an index or key * @param {boolean} noComplain - Whereter or not to complain about non existing variables * @returns * @memberof Interpreter */ public deleteVariable( variableName: string, variableKey: string | number | null, noComplain: boolean, ) { // Set the correct keys let name = variableName; let objectKey = typeof variableKey === 'string' ? variableKey : null; let arrayIndex = typeof variableKey === 'number' ? variableKey : null; // Read if the value is already present in the scope let existingValue: TclVariable | null = this.scope.resolve(name); // Check if a value is there if (existingValue === null && !noComplain) throw new TclError(`can't unset "${variableName}": no such variable`); // Check if an object key was parsed if (objectKey !== null) { // If a value is already present, check if it is indeed an object if (!(existingValue instanceof TclObject)) throw new TclError( `cant' unset "${variableName}": variable isn't object`, ); // Update the object with the value and return existingValue.set(objectKey, undefined); return; } // Check an array index was parsed else if (arrayIndex !== null) { // If a value is already present, check if it is indeed an array if (!(existingValue instanceof TclArray)) throw new TclError( `cant' unset "${variableName}": variable isn't array`, ); // Update the array with the value and return existingValue.set(arrayIndex, undefined); return; } this.scope.undefine(name); return; } // Not needed /** * Function to go over a string and solve all square bracket subexpressions accordingly * * @param {string} input - The string to loop over * @param {number=0} position - The starting position in the string * @returns Promise - The found and processed results */ /* private async deepProcessSquareBrackets( input: string, position: number = 0, ): Promise<string | TclVariable> { // Initialize output string let output = ''; // Intitialize variable for every found variable let toProcess: FoundVariable | null; // Keep going as long as there are variables while ( (toProcess = await this.resolveFirstSquareBracket(input, position)) ) { // Add the string until the first found variable while (position < toProcess.startPosition) { output += input.charAt(position); position++; } // Jump to the end of the variable position = toProcess.endPosition; // Return the full value if it corrresponds to the entire string if (toProcess.raw === input) return toProcess.value; // Otherwise add the result to the output output += toProcess.value.getValue(); } // Add the last bit of the string while (position < input.length) { output += input.charAt(position); position++; } return output; } */ /** * Function to find and resolve the first subexpression that it hits * * @private * @param {string} input - The input string * @param {number} position - Where in the string to start searching * @returns {(Promise<FoundVariable | null>)} - The processed output * @memberof Interpreter */ private async resolveFirstSquareBracket( input: string, position: number, ): Promise<FoundVariable | null> { // Setup output buffer let outbuf = { originalString: '', startPosition: 0, endPosition: 0, expression: '', value: new TclVariable(''), }; // Setup necessary variables let char = input.charAt(position); let depth = 0; // Function to progress one char function read(appendOnOriginal: boolean) { if (appendOnOriginal) outbuf.originalString += char; position += 1; char = input.charAt(position); } // Keep reading string until a [ is found while (char !== '[' && position < input.length) { if (char === '\\') read(false); read(false); } // Check if it was not the end of the string that stopped us if (char !== '[') return null; char = <string>char; outbuf.startPosition = position; // Eat the [ character read(true); depth = 1; // While input is abvailable while (position < input.length) { // Change depth based on character if (char === '[') { depth++; } if (char === ']') { depth--; if (depth === 0) break; } // Handle escapes if (char === '\\') { outbuf.expression += char; read(true); } // Move to the next char outbuf.expression += char; read(true); } // Check if depth is zero after loop, otherwise throw error if (depth !== 0) throw new TclError('incorrect amount of square brackets'); read(true); let interpreter = new Interpreter( this.tcl, outbuf.expression, new Scope(this.scope), ); outbuf.value = await interpreter.run(); // And return the solved variable with extra info return { raw: outbuf.originalString, startPosition: outbuf.startPosition, endPosition: position, value: outbuf.value, }; } /** * Function to replace all backslash sequences correctly * * @private * @param {string} input - The string to process * @returns {string} - The processed string * @memberof Interpreter */ private processBackSlash(input: string): string { // Intialize all regexes let simpleBackRegex = /\\(?<letter>[abfnrtv])/g; let octalBackRegex = /\\0(?<octal>[0-7]{0,2})/g; let unicodeBackRegex = /\\u(?<hexcode>[0-9a-fA-F]{1,4})/g; let hexBackRegex = /\\x(?<hexcode>[0-9a-fA-F]{1,2})/g; let cleanUpBackRegex = /\\(?<character>.)/g; // Function to convert a number to the corresponding character function codeToChar(hexCode: number): string { return String.fromCharCode(hexCode); } // Replace all simple backslash sequences input = input.replace( simpleBackRegex, (...args: any[]): string => { let groups = args[args.length - 1]; // Replace with the corresponding character depending on the letter switch (groups.letter) { case 'a': return codeToChar(0x07); case 'b': return codeToChar(0x08); case 'f': return codeToChar(0x0c); case 'n': return codeToChar(0x0a); case 'r': return codeToChar(0x0d); case 't': return codeToChar(0x09); case 'v': return codeToChar(0x0b); default: throw new TclError('program hit unreachable point'); } }, ); // Replace the octal values input = input.replace( octalBackRegex, (...args: any[]): string => { let groups = args[args.length - 1]; let octal = parseInt(groups.octal, 8); return codeToChar(octal); }, ); // Replace the unicode values input = input.replace( unicodeBackRegex, (...args: any[]): string => { let groups = args[args.length - 1]; let hex = parseInt(groups.hexcode.toLowerCase(), 16); return codeToChar(hex); }, ); // Replace the hexadecimal values input = input.replace( hexBackRegex, (...args: any[]): string => { let groups = args[args.length - 1]; let hex = parseInt(groups.hexcode.toLowerCase(), 16); return codeToChar(hex); }, ); // Replace all other backslashes with only the second character input = input.replace( cleanUpBackRegex, (...args: any[]): string => { let groups = args[args.length - 1]; return groups.character; }, ); return input; } } /** * Checks if a variable is a number * * @export * @param {*} input - The variable to check * @returns */ export function isNumber(input: any) { return !isNaN(<number>(<unknown>input)) && !isNaN(parseInt(input, 10)); } /** * An interface for holding a found variable * * @interface FoundVariable */ interface FoundVariable { raw: string; startPosition: number; endPosition: number; value: TclVariable; }