Merge pull request #2 from chrisvrose/exec-terminal

Execution Tasks
This commit is contained in:
2025-07-20 04:35:07 +01:00
committed by GitHub
13 changed files with 3284 additions and 4911 deletions

View File

@@ -13,14 +13,25 @@ A simple language server based VSCode Extension for the (Branflakes?) (BrainFuck
### Execution
Use the command to execute the code.
Issue is, because BF is a **turing complete** language, there is no way to know if the program will terminate or not. Hence for now, the command may lead to infinite execution.
If the program requires input, it will be requested as a prompt.
Use the BF execute task to execute the code.
Either run the "current file" task, or create a customized task with the required file.
I/O is done on the created terminal window.
TODO: Implement a timeout.
There is also an older command to run the code, where output is shown as a status message. Here, if the program requires input, it will be requested as a prompt.
### Changelog
#### 0.3.0
![command](./assets/command.gif)
- Added a task for execution
- Press Control C to halt it while its waiting for input
- Close task to abort execution
- Detail: The program will halt between loop iterations.
- Migrated the run command to `bf.execute.old`
#### 0.2.1
- Change category

BIN
assets/command.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

View File

@@ -0,0 +1,20 @@
import { EventEmitter } from 'vscode';
import InputStrategy from './input/InputStrategy';
import {BranFlakesExecutorVisitor} from './exec/BranFlakesExecutorVisitor';
export class BranFlakesStreamingExecutor {
constructor(private fileData: string, private fileName: string = 'fileName:dummy', private emitter: EventEmitter<string>,
private inputStrategy: InputStrategy
) { }
async run() {
const finalOutput = await BranFlakesExecutorVisitor.run(
this.fileData,
this.fileName,
this.inputStrategy,
async (str) => { this.emitter.fire(str); }
);
}
}

View File

@@ -1,140 +0,0 @@
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
import { LoopStmtContext } from './generated/bfParser';
import { bfVisitor } from './generated/bfVisitor';
import { DiagnosticSeverity } from 'vscode-languageclient';
import { getTree } from './BranFlakesParseRunner';
import { RuleNode } from 'antlr4ts/tree/RuleNode';
import type InputStrategy from './input/InputStrategy';
export class BranFlakesExecutorVisitor
extends AbstractParseTreeVisitor<Promise<void>>
implements bfVisitor<Promise<void>>
{
/**
*
* @param input Input string
* @param inputPtr Input pointer to start from
*/
constructor(
private inputStrategy: InputStrategy,
private logger: (val: string) => Thenable<string>,
private inputPtr: number = 0
) {
super();
}
// /**
// * The memory cells (Can work with negative cells this way)
// */
// private cells: Map<number, number> = new Map();
private byteArraySize: number = 30000;
private byteArray: Int8Array = new Int8Array(this.byteArraySize);
/**
* Pointer
*/
private ptr: number = 0;
/** Output string */
private outputStr: string = '';
defaultResult() {
return Promise.resolve();
}
/**
* Run a file
* @param text
* @param fn
* @param inputStrategy
* @returns
*/
static async run(
text: string,
fn: string,
inputStrategy: InputStrategy,
logger: (str: string) => Thenable<string>
) {
//get tree and issues
const { tree, issues } = getTree(text, fn);
//get only errors
const x = issues.filter(e => e.type === DiagnosticSeverity.Error);
//if any error, drop
if (x.length > 0) {
throw Error('Errors exist');
}
// make visitor
const vis = new BranFlakesExecutorVisitor(inputStrategy, logger);
//visit the tree
await vis.visit(tree);
//get output
return vis.outputStr;
}
getCell(pointerIndex: number) {
return this.byteArray[pointerIndex];
}
setCell(pointerIndex: number, value: number): void {
this.byteArray[pointerIndex] = value;
}
async visitLoopStmt(ctx: LoopStmtContext) {
while ((this.getCell(this.ptr) ?? 0) !== 0) {
await this.visitChildren(ctx);
}
}
async visitPtrLeft() {
this.ptr = (this.ptr + this.byteArraySize - 1) % this.byteArraySize;
}
async visitPtrRight() {
this.ptr = (this.ptr + this.byteArraySize + 1) % this.byteArraySize;
}
async visitPtrIncr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 1) % 256);
}
async visitPtrDecr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 255) % 256);
}
async visitOutputStmt() {
const val = this.getCell(this.ptr) ?? 0;
const str = String.fromCharCode(val);
this.outputStr += str;
}
async visitInputStmt() {
//get char
const char = (await this.inputStrategy.getInput()) ?? 0;
//increment the input pointer after this
this.inputPtr++;
this.setCell(this.ptr, char);
}
// override for maintaining async
async visitChildren(node: RuleNode): Promise<void> {
let result = this.defaultResult();
await result;
let n = node.childCount;
for (let i = 0; i < n; i++) {
if (!this.shouldVisitNextChild(node, result)) {
break;
}
let c = node.getChild(i);
let childResult = c.accept(this);
result = this.aggregateResult(result, childResult);
await result;
}
return Promise.resolve();
}
// override for maintaining async
protected async aggregateResult(
aggregate: Promise<void>,
nextResult: Promise<void>
): Promise<void> {
await aggregate;
return nextResult;
}
}

View File

@@ -3,27 +3,27 @@ import type { BranFlakesCommand } from './BranFlakesCommand';
import { VSCodePromptInputStrategy } from '../input/VSCodePromptInputStrategy';
export class CompileBranFlakesCommand implements BranFlakesCommand {
getCommandName() {
return 'bf.execute';
}
getCommandHandler() {
return async () => {
const text = window.activeTextEditor.document.getText();
const fn = window.activeTextEditor.document.fileName;
const inputStrategy = new VSCodePromptInputStrategy(
window.showInputBox
);
const { BranFlakesExecutorVisitor } = await import(
'../BranFlakesExecutorVisitor'
);
getCommandName() {
return 'bf.execute.old';
}
getCommandHandler() {
return async () => {
const text = window.activeTextEditor.document.getText();
const fn = window.activeTextEditor.document.fileName;
const inputStrategy = new VSCodePromptInputStrategy(
window.showInputBox
);
const { BranFlakesExecutorVisitor } = await import(
'../exec/BranFlakesExecutorVisitor'
);
const output = await BranFlakesExecutorVisitor.run(
text,
fn,
inputStrategy,
window.showInformationMessage
);
await window.showInformationMessage(`Output: ${output}`);
};
}
const output = await BranFlakesExecutorVisitor.run(
text,
fn,
inputStrategy,
window.showInformationMessage
);
await window.showInformationMessage(`Output: ${output}`);
};
}
}

View File

@@ -0,0 +1,11 @@
export class AbortClassRequestor{
private abortRequest=false;
requestAbort(){
this.abortRequest=true;
}
isAborted(){
return this.abortRequest;
}
}

View File

@@ -0,0 +1,147 @@
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
import { LoopStmtContext } from '../generated/bfParser';
import { bfVisitor } from '../generated/bfVisitor';
import { DiagnosticSeverity } from 'vscode-languageclient';
import { getTree } from '../BranFlakesParseRunner';
import { RuleNode } from 'antlr4ts/tree/RuleNode';
import type InputStrategy from '../input/InputStrategy';
import { AbortClassRequestor } from './AbortRequestor';
export class BranFlakesExecutorVisitor
extends AbstractParseTreeVisitor<Promise<void>>
implements bfVisitor<Promise<void>> {
/**
*
* @param input Input string
* @param inputPtr Input pointer to start from
*/
constructor(
private inputStrategy: InputStrategy,
private logger: (val: string) => Thenable<void>,
private abortRequestor?: AbortClassRequestor,
private inputPtr: number = 0
) {
super();
}
// /**
// * The memory cells (Can work with negative cells this way)
// */
// private cells: Map<number, number> = new Map();
private byteArraySize: number = 30000;
private byteArray: Int8Array = new Int8Array(this.byteArraySize);
/**
* Pointer
*/
private ptr: number = 0;
/** Output string */
private outputStr: string = '';
defaultResult() {
return Promise.resolve();
}
/**
* Run a file
* @param text
* @param fn
* @param inputStrategy
* @returns
*/
static async run(
text: string,
fn: string,
inputStrategy: InputStrategy,
logger: (str: string) => Thenable<void>,
aborter?: AbortClassRequestor
) {
//get tree and issues
const { tree, issues } = getTree(text, fn);
//get only errors
const x = issues.filter(e => e.type === DiagnosticSeverity.Error);
//if any error, drop
if (x.length > 0) {
throw Error('Errors exist');
}
// make visitor
const vis = new BranFlakesExecutorVisitor(inputStrategy, logger, aborter);
//visit the tree
await vis.visit(tree);
//get output
return vis.outputStr;
}
getCell(pointerIndex: number) {
return this.byteArray[pointerIndex];
}
setCell(pointerIndex: number, value: number): void {
this.byteArray[pointerIndex] = value;
}
async visitLoopStmt(ctx: LoopStmtContext) {
while ((this.getCell(this.ptr) ?? 0) !== 0) {
await this.visitChildren(ctx);
}
}
async visitPtrLeft() {
this.ptr = (this.ptr + this.byteArraySize - 1) % this.byteArraySize;
}
async visitPtrRight() {
this.ptr = (this.ptr + this.byteArraySize + 1) % this.byteArraySize;
}
async visitPtrIncr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 1) % 256);
}
async visitPtrDecr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 255) % 256);
}
async visitOutputStmt() {
const val = this.getCell(this.ptr) ?? 0;
const str = String.fromCharCode(val);
this.outputStr += str;
await this.logger(str);
}
async visitInputStmt() {
//get char
const char = (await this.inputStrategy.getInput()) ?? 0;
if (char === 3) { throw Error('Halt input wait'); }
//increment the input pointer after this
this.inputPtr++;
this.setCell(this.ptr, char);
}
// override for maintaining async
async visitChildren(node: RuleNode): Promise<void> {
let result = this.defaultResult();
await result;
let n = node.childCount;
for (let i = 0; i < n; i++) {
if (!this.shouldVisitNextChild(node, result)) {
break;
}
let c = node.getChild(i);
let childResult = c.accept(this);
result = this.aggregateResult(result, childResult);
await result;
}
// break for any close requests
return new Promise((res, rej) => {
setTimeout(() => { if (this.abortRequestor?.isAborted()??false) {rej('aborted');} else {res(undefined);} }, 0);
});
}
// override for maintaining async
protected async aggregateResult(
aggregate: Promise<void>,
nextResult: Promise<void>
): Promise<void> {
await aggregate;
return nextResult;
}
}

View File

@@ -4,8 +4,8 @@
* ------------------------------------------------------------------------------------------ */
import * as path from 'path';
import { commands } from 'vscode';
import type { ExtensionContext } from 'vscode';
import { commands, tasks, workspace,window } from 'vscode';
import type { Disposable, ExtensionContext } from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
@@ -13,9 +13,12 @@ import {
TransportKind,
} from 'vscode-languageclient';
import { CompileBranFlakesCommand } from './command/CompileBranFlakesCommand';
import { CustomExecutionTaskProvider } from './task/CustomExecutionTerminal';
let client: LanguageClient;
let bfRunTaskProvider: Disposable;
export function activate(context: ExtensionContext) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(
@@ -48,10 +51,13 @@ export function activate(context: ExtensionContext) {
commands.registerCommand(
branFlakesCommand.getCommandName(),
branFlakesCommand.getCommandHandler()
)
),
);
}
// Create the language client and start the client.
client = new LanguageClient(
'brainfucklanguageserver',
@@ -62,11 +68,17 @@ export function activate(context: ExtensionContext) {
// Start the client. This will also launch the server
client.start();
const workspaceRoot = (workspace.workspaceFolders && (workspace.workspaceFolders.length > 0))
? workspace.workspaceFolders[0].uri.fsPath : undefined;
bfRunTaskProvider = tasks.registerTaskProvider(CustomExecutionTaskProvider.type, new CustomExecutionTaskProvider(workspaceRoot));
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
bfRunTaskProvider?.dispose();
client?.stop();
}

View File

@@ -1,74 +1,152 @@
import path = require('node:path');
import * as vscode from 'vscode';
import * as path from 'path';
import { BranFlakesExecutorVisitor } from '../exec/BranFlakesExecutorVisitor';
import { AbortClassRequestor } from '../exec/AbortRequestor';
interface BFRunTaskDefinition extends vscode.TaskDefinition {
file?: string;
}
export class CustomExecutionTaskProvider implements vscode.TaskProvider {
static type: string = 'bf-run';
tasks: vscode.Task[] | undefined;
constructor(private workspaceRoot: string | undefined) {
}
export class CustomExecutionTaskProvider implements vscode.TaskProvider{
provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult<vscode.Task[]> {
throw new Error('Method not implemented.');
}
resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult<vscode.Task> {
const taskDefinition = {};
throw new Error('5');
// return new vscode.Task()
if (this.tasks !== undefined) { return this.tasks; }
this.tasks = [this.getTaskFromDefinition(undefined)];
return this.tasks;
}
getTaskFromDefinition(fileName: string | undefined): vscode.Task {
const definition: BFRunTaskDefinition = {
type: CustomExecutionTaskProvider.type,
file: fileName
};
return new vscode.Task(definition, vscode.TaskScope.Workspace, `bf: run: current file`, CustomExecutionTaskProvider.type,
new vscode.CustomExecution(async () => {
return new CustomBuildTaskTerminal(definition.file);
})
);
}
resolveTask(_task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult<vscode.Task> {
const definition: BFRunTaskDefinition = <any>_task.definition;
const fileNameRecovered = definition.file;
const taskName = `bf: run: ` + (fileNameRecovered ?? 'current file');
return new vscode.Task(definition, vscode.TaskScope.Workspace, taskName, CustomExecutionTaskProvider.type,
new vscode.CustomExecution(async () => {
return new CustomBuildTaskTerminal(definition.file);
})
);
}
}
function replaceLFWithCRLF(data: string) {
return data.replace(/(?<!\r)\n/gm, '\r\n');
}
class CustomBuildTaskTerminal implements vscode.Pseudoterminal {
private writeEmitter = new vscode.EventEmitter<string>();
onDidWrite: vscode.Event<string> = this.writeEmitter.event;
private closeEmitter = new vscode.EventEmitter<number>();
private readEmitter = new vscode.EventEmitter<void>();
inputQueue: number[] = [];
private openDocument: vscode.TextDocument | undefined;
onDidWrite: vscode.Event<string> = this.writeEmitter.event;
onDidClose?: vscode.Event<number> = this.closeEmitter.event;
abortRequestor = new AbortClassRequestor();
private fileWatcher: vscode.FileSystemWatcher | undefined;
constructor(private workspaceRoot: string, private flavor: string, private flags: string[], private getSharedState: () => string | undefined, private setSharedState: (state: string) => void) {
handleInput(data: string): void {
// this.writeEmitter.fire(`Echo(${data.length})` + data);
const newData = [...data].map(e => e.charCodeAt(0));
console.log('new input', newData);
this.inputQueue.push(...newData);
this.writeEmitter.fire(replaceLFWithCRLF(data));
this.readEmitter.fire();
}
open(initialDimensions: vscode.TerminalDimensions | undefined): void {
constructor(private fileName?: string) {
}
open(_initialDimensions: vscode.TerminalDimensions | undefined): void {
// At this point we can start using the terminal.
if (this.flags.indexOf('watch') > -1) {
const pattern = path.join(this.workspaceRoot, 'customBuildFile');
this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern);
this.fileWatcher.onDidChange(() => this.doBuild());
this.fileWatcher.onDidCreate(() => this.doBuild());
this.fileWatcher.onDidDelete(() => this.doBuild());
this.openDocumentForTask().then(this.doExecution.bind(this));
}
getPath(fileLocationString: string | undefined, fileName: string) {
if (fileLocationString === undefined) { return vscode.Uri.file(fileName); }
return vscode.Uri.file(path.resolve(fileLocationString, fileName));
}
private async openDocumentForTask() {
let openDocument: vscode.TextDocument;
if (this.fileName !== undefined) {
try {
const fileLocationPathString = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
const finalPath = this.getPath(fileLocationPathString, this.fileName);
openDocument = await vscode.workspace.openTextDocument(finalPath);
} catch (e) {
vscode.window.showErrorMessage('Failed to open file ' + this.fileName);
this.closeEmitter.fire(2);
}
} else {
openDocument = vscode.window.activeTextEditor.document;
}
this.doBuild();
this.openDocument = openDocument;
}
close(): void {
// The terminal has been closed. Shutdown the build.
if (this.fileWatcher) {
this.fileWatcher.dispose();
}
console.log('Forced close');
this.abortRequestor.requestAbort();
}
private async doBuild(): Promise<void> {
return new Promise<void>((resolve) => {
this.writeEmitter.fire('Starting build...\r\n');
let isIncremental = this.flags.indexOf('incremental') > -1;
if (isIncremental) {
if (this.getSharedState()) {
this.writeEmitter.fire('Using last build results: ' + this.getSharedState() + '\r\n');
} else {
isIncremental = false;
this.writeEmitter.fire('No result from last build. Doing full build.\r\n');
}
}
private async doExecution(): Promise<void> {
this.writeEmitter.fire('[bf] Requested execution of ' + (this.fileName ?? 'active file') + '\r\n');
const cus = this;
try {
await BranFlakesExecutorVisitor.run(this.openDocument.getText(),
this.openDocument.uri.fsPath,
{
getInput() {
return new Promise((res, rej) => {
if (cus.inputQueue.length > 0) {
const char = cus.inputQueue.shift();
res(char);
} else {
const dispose: vscode.Disposable[] = [];
cus.readEmitter.event(e => {
const char = cus.inputQueue.shift();
//clear the earliest disposable
dispose.shift()?.dispose();
res(char);
}, null, dispose);
}
});
},
},
async (data) => {
this.writeEmitter.fire(replaceLFWithCRLF(data));
},this.abortRequestor
);
this.closeEmitter.fire(0);
} catch (e) {
this.closeEmitter.fire(1);
}
// Since we don't actually build anything in this example set a timeout instead.
setTimeout(() => {
const date = new Date();
this.setSharedState(date.toTimeString() + ' ' + date.toDateString());
this.writeEmitter.fire('Build complete.\r\n\r\n');
if (this.flags.indexOf('watch') === -1) {
this.closeEmitter.fire(0);
resolve();
}
}, isIncremental ? 1000 : 4000);
});
}
}

View File

@@ -16,7 +16,8 @@ const config = {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
sourceMapFilename: '[name].js.map',
devtoolModuleFilenameTemplate: '../[resource-path]',
},
// devtool: 'source-map',
externals: {
@@ -27,6 +28,7 @@ const config = {
innerGraph:true,
usedExports:true
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js'],

7596
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"author": "Atreya Bain",
"license": "MIT",
"publisher": "atreyabain",
"version": "0.2.1",
"version": "0.3.1",
"icon": "assets/128.png",
"categories": ["Programming Languages","Linters"],
"keywords": [
@@ -29,7 +29,7 @@
"type": "git",
"url": "https://github.com/chrisvrose/bf-server"
},
"main": "./client/dist/extension",
"main": "./client/dist/extension.js",
"contributes": {
"languages": [
{
@@ -57,7 +57,7 @@
],
"commands": [
{
"command": "bf.execute",
"command": "bf.execute.old",
"title": "BF: Execute",
"when": "editorLangId == bf",
"enablement": "editorLangId == bf"
@@ -85,7 +85,18 @@
"description": "Traces the communication between VS Code and the language server."
}
}
}
},
"taskDefinitions": [
{
"type": "bf-run",
"properties": {
"file":{
"type":"string",
"description": "The BF file to be executed. Can be omitted to run current file"
}
}
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",

View File

@@ -5,7 +5,8 @@
"lib": ["ES2019"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true
"sourceMap": true,
"strict": true
},
"include": [
"src"