├── .gitignore ├── src ├── electron │ ├── installer.ico │ ├── webpack.config.ts │ └── index.ts ├── app │ ├── content │ │ ├── appicon.png │ │ ├── applogo.png │ │ ├── buttons │ │ │ ├── cut.png │ │ │ ├── new.png │ │ │ ├── run.png │ │ │ ├── copy.png │ │ │ ├── debug.png │ │ │ ├── paste.png │ │ │ ├── redo.png │ │ │ ├── step.png │ │ │ ├── stop.png │ │ │ └── undo.png │ │ └── index.ejs │ ├── components │ │ ├── debug │ │ │ ├── memory │ │ │ │ ├── header.png │ │ │ │ └── index.tsx │ │ │ ├── callstack │ │ │ │ ├── header.png │ │ │ │ └── index.tsx │ │ │ └── style.css │ │ ├── editor │ │ │ └── documentation │ │ │ │ ├── header.png │ │ │ │ ├── style.css │ │ │ │ └── index.tsx │ │ ├── common │ │ │ ├── custom-editor │ │ │ │ ├── images │ │ │ │ │ ├── wavy-line.gif │ │ │ │ │ └── intellisense-background.png │ │ │ │ ├── styles │ │ │ │ │ ├── style.css │ │ │ │ │ └── monaco-override.css │ │ │ │ ├── services │ │ │ │ │ ├── hover-service.ts │ │ │ │ │ └── completion-service.ts │ │ │ │ └── index.tsx │ │ │ ├── text-window │ │ │ │ └── style.css │ │ │ ├── graphics-window │ │ │ │ ├── style.css │ │ │ │ └── index.tsx │ │ │ ├── toolbar-divider │ │ │ │ └── index.tsx │ │ │ ├── shapes │ │ │ │ ├── text.ts │ │ │ │ ├── rectangle.ts │ │ │ │ ├── line.ts │ │ │ │ ├── ellipse.ts │ │ │ │ ├── triangle.ts │ │ │ │ ├── base-shape.ts │ │ │ │ └── shapes-plugin.ts │ │ │ ├── modal │ │ │ │ ├── style.css │ │ │ │ └── index.tsx │ │ │ ├── toolbar-button │ │ │ │ └── index.tsx │ │ │ └── master-layout │ │ │ │ ├── style.css │ │ │ │ └── index.tsx │ │ └── run │ │ │ └── index.tsx │ ├── app.tsx │ ├── webpack.config.ts │ ├── store.ts │ └── editor-utils.ts ├── strings │ ├── compiler.ts │ ├── editor.ts │ └── diagnostics.ts └── compiler │ ├── runtime │ ├── libraries │ │ ├── clock.ts │ │ ├── program.ts │ │ ├── stack.ts │ │ ├── array.ts │ │ ├── math.ts │ │ └── text-window.ts │ ├── values │ │ ├── base-value.ts │ │ ├── array-value.ts │ │ ├── string-value.ts │ │ └── number-value.ts │ └── libraries.ts │ ├── utils │ ├── notifications.ts │ ├── diagnostics.ts │ └── compiler-utils.ts │ ├── syntax │ ├── tokens.ts │ └── ranges.ts │ ├── binding │ └── modules-binder.ts │ ├── emitting │ └── temp-labels-remover.ts │ ├── services │ ├── hover-service.ts │ └── completion-service.ts │ ├── compilation.ts │ └── execution-engine.ts ├── tests ├── index.ts ├── webpack.config.ts └── compiler │ ├── runtime │ ├── statements │ │ ├── labels.ts │ │ ├── while-loop.ts │ │ ├── if-statements.ts │ │ └── for-loop.ts │ ├── submodules.ts │ ├── expressions │ │ ├── array-access.ts │ │ ├── negation.ts │ │ ├── to-boolean.ts │ │ ├── logical-operators.ts │ │ ├── addition.ts │ │ └── subtraction.ts │ ├── libraries │ │ ├── clock.ts │ │ ├── program.ts │ │ ├── stack.ts │ │ ├── array.ts │ │ └── text-window.ts │ ├── stepping-through.ts │ └── libraries-metadata.ts │ ├── binding │ └── module-binder.ts │ ├── tests-list.ts │ ├── services │ ├── completion-service.ts │ └── hover-service.ts │ └── syntax │ ├── scanner.ts │ └── statements-parser.ts ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── README.md ├── appveyor.yml ├── tsconfig.json ├── tslint.json ├── LICENSE ├── package.json ├── gulpfile.ts └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ -------------------------------------------------------------------------------- /src/electron/installer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/electron/installer.ico -------------------------------------------------------------------------------- /src/app/content/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/appicon.png -------------------------------------------------------------------------------- /src/app/content/applogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/applogo.png -------------------------------------------------------------------------------- /src/app/content/buttons/cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/cut.png -------------------------------------------------------------------------------- /src/app/content/buttons/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/new.png -------------------------------------------------------------------------------- /src/app/content/buttons/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/run.png -------------------------------------------------------------------------------- /src/app/content/buttons/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/copy.png -------------------------------------------------------------------------------- /src/app/content/buttons/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/debug.png -------------------------------------------------------------------------------- /src/app/content/buttons/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/paste.png -------------------------------------------------------------------------------- /src/app/content/buttons/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/redo.png -------------------------------------------------------------------------------- /src/app/content/buttons/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/step.png -------------------------------------------------------------------------------- /src/app/content/buttons/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/stop.png -------------------------------------------------------------------------------- /src/app/content/buttons/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/content/buttons/undo.png -------------------------------------------------------------------------------- /src/app/components/debug/memory/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/components/debug/memory/header.png -------------------------------------------------------------------------------- /src/app/components/debug/callstack/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/components/debug/callstack/header.png -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import * as sourceMapSupport from "source-map-support"; 2 | sourceMapSupport.install(); 3 | 4 | import "./compiler/tests-list"; 5 | -------------------------------------------------------------------------------- /src/app/components/editor/documentation/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/components/editor/documentation/header.png -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/images/wavy-line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/components/common/custom-editor/images/wavy-line.gif -------------------------------------------------------------------------------- /src/app/components/common/text-window/style.css: -------------------------------------------------------------------------------- 1 | .text-window { 2 | padding: 5px; 3 | overflow-y: auto; 4 | word-break: break-all; 5 | font-size: 18px; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/images/intellisense-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb/SmallBasic-Online/HEAD/src/app/components/common/custom-editor/images/intellisense-background.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false, 3 | "editor.tabSize": 4, 4 | "editor.insertSpaces": true, 5 | "typescriptHero.resolver.stringQuoteStyle": "\"", 6 | "typescriptHero.resolver.organizeOnSave": true 7 | } -------------------------------------------------------------------------------- /src/app/components/common/graphics-window/style.css: -------------------------------------------------------------------------------- 1 | .graphics-window { 2 | padding-left: 5px; 3 | overflow-y: auto; 4 | word-break: break-all; 5 | font-size: 18px; 6 | height: 100%; 7 | } 8 | 9 | #graphics-container { 10 | width: 100%; 11 | height: 100%; 12 | } -------------------------------------------------------------------------------- /src/app/components/debug/style.css: -------------------------------------------------------------------------------- 1 | .debug-component { 2 | height: 100%; 3 | } 4 | 5 | .debug-component .editor-container { 6 | height: calc(50% - 10px); 7 | } 8 | 9 | .debug-component .text-window-container { 10 | margin-top: 10px; 11 | height: calc(50% - 10px); 12 | } -------------------------------------------------------------------------------- /src/app/content/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/styles/style.css: -------------------------------------------------------------------------------- 1 | .wavy-line { 2 | display: inline-block; 3 | position: relative; 4 | background: url("../images/wavy-line.gif") bottom repeat-x; 5 | } 6 | 7 | .error-line-glyph { 8 | background: red; 9 | } 10 | 11 | .debugger-line-highlight { 12 | background: rgb(255, 255, 148); 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmallBasic-Online 2 | 3 | An open-source IDE/runtime for the Small Basic programming language. 4 | 5 | To launch a local instance of the editor: 6 | * Have node/npm installed. 7 | * `npm i` to install the required packages 8 | * `npm run gulp watch-source` to start the webpack server. 9 | * Point your browser to [http://localhost:8080](http://localhost:8080). 10 | * webpack should print out that url if another one was used. 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "8.4.0" 3 | 4 | matrix: 5 | # debug configuration 6 | - configuration: debug 7 | 8 | # release configuration 9 | - configuration: release 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - npm install 14 | 15 | build_script: 16 | - npm run gulp package-%configuration% 17 | 18 | test_script: 19 | - npm run gulp run-tests-%configuration% 20 | -------------------------------------------------------------------------------- /src/app/components/common/toolbar-divider/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export class ToolbarDivider extends React.Component { 4 | public render(): JSX.Element { 5 | const style: React.CSSProperties = { 6 | width: "1px", 7 | height: "100%", 8 | minWidth: "1px", 9 | background: "#bcbcbc" 10 | }; 11 | 12 | return
; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as webpack from "webpack"; 3 | import { factory } from "../build/webpack.config"; 4 | 5 | export default function (env: any): webpack.Configuration { 6 | return factory({ 7 | env: env, 8 | entryPath: { 9 | "tests": path.resolve(__dirname, "index.ts") 10 | }, 11 | outputRelativePath: "tests", 12 | target: "node" 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build-tests-debug", 6 | "type": "shell", 7 | "command": "npm", 8 | "args": [ 9 | "run", 10 | "gulp", 11 | "build-tests-debug" 12 | ], 13 | "options": { 14 | "cwd": "${workspaceFolder}" 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/electron/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as webpack from "webpack"; 3 | import { factory } from "../../build/webpack.config"; 4 | 5 | export default function (env: any): webpack.Configuration { 6 | return factory({ 7 | env: env, 8 | entryPath: { 9 | "index": path.resolve(__dirname, "index.ts") 10 | }, 11 | outputRelativePath: "electron", 12 | target: "electron-main" 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /tests/compiler/runtime/statements/labels.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Statements.Labels", () => { 5 | it("can go to labels", () => { 6 | verifyRuntimeResult(` 7 | GoTo two 8 | one: 9 | TextWindow.WriteLine(1) 10 | GoTo three 11 | two: 12 | TextWindow.WriteLine(2) 13 | GoTo one 14 | three: 15 | TextWindow.WriteLine(3)`, 16 | [], 17 | ["2", "1", "3"]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Tests", 8 | "preLaunchTask": "build-tests-debug", 9 | "program": "${workspaceFolder}/node_modules/jasmine/bin/jasmine.js", 10 | "args": [ 11 | "${workspaceFolder}/out/tests/tests.js" 12 | ], 13 | "outFiles": [ 14 | "${workspaceFolder}/out/tests/**" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tests/compiler/runtime/submodules.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../helpers"; 3 | 4 | describe("Compiler.Runtime.SubModules", () => { 5 | it("calls submodules from within submodules", () => { 6 | verifyRuntimeResult(` 7 | Sub A 8 | TextWindow.WriteLine("hello from A") 9 | B() 10 | EndSub 11 | Sub B 12 | TextWindow.WriteLine("hello from B") 13 | EndSub 14 | A()`, 15 | [], 16 | [ 17 | "hello from A", 18 | "hello from B" 19 | ]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/components/editor/documentation/style.css: -------------------------------------------------------------------------------- 1 | .documentation { 2 | overflow-y: scroll; 3 | } 4 | 5 | .documentation ul { 6 | list-style-type: none; 7 | } 8 | 9 | .documentation .library-class { 10 | font-weight: bold; 11 | font-size: medium; 12 | cursor: pointer; 13 | } 14 | 15 | .documentation .library-class-name:hover { 16 | color: #74c1df; 17 | } 18 | 19 | .documentation .library-members { 20 | font-weight: bold; 21 | font-size: small; 22 | cursor: pointer; 23 | } 24 | 25 | .documentation .library-member-name:hover { 26 | color: #74c1df; 27 | } 28 | 29 | .documentation .description { 30 | font-weight: normal; 31 | } -------------------------------------------------------------------------------- /src/strings/compiler.ts: -------------------------------------------------------------------------------- 1 | // This file is generated through a build task. Do not edit by hand. 2 | 3 | export module CompilerResources { 4 | export const SyntaxNodes_Identifier = "identifier"; 5 | export const SyntaxNodes_StringLiteral = "string"; 6 | export const SyntaxNodes_NumberLiteral = "number"; 7 | export const SyntaxNodes_Comment = "comment"; 8 | export const SyntaxNodes_Label = "label"; 9 | export const SyntaxNodes_Expression = "expression"; 10 | export const ProgramKind_TextWindow = "Text Window"; 11 | export const ProgramKind_Turtle = "Turtle"; 12 | 13 | export function get(key: string): string { 14 | return (CompilerResources)[key]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/electron/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from "electron"; 2 | 3 | let mainWindow: Electron.BrowserWindow | null = null; 4 | 5 | function createWindow(): void { 6 | mainWindow = new BrowserWindow({ 7 | height: 600, 8 | width: 800 9 | }); 10 | 11 | mainWindow.setMenu(null); 12 | mainWindow.loadURL(`file://${__dirname}/index.html`); 13 | 14 | mainWindow.on("closed", () => { 15 | mainWindow = null; 16 | }); 17 | } 18 | 19 | app.on("ready", createWindow); 20 | 21 | app.on("activate", () => { 22 | if (mainWindow === null) { 23 | createWindow(); 24 | } 25 | }); 26 | 27 | app.on("window-all-closed", () => { 28 | if (process.platform !== "darwin") { 29 | app.quit(); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /tests/compiler/binding/module-binder.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyCompilationErrors } from "../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Binding.ModuleBinder", () => { 7 | it("reports sub-modules with duplicate names", () => { 8 | verifyCompilationErrors(` 9 | Sub x 10 | EndSub 11 | 12 | Sub y 13 | EndSub 14 | 15 | Sub x 16 | EndSub`, 17 | // Sub x 18 | // ^ 19 | // Another sub-module with the same name 'x' is already defined. 20 | new Diagnostic(ErrorCode.TwoSubModulesWithTheSameName, CompilerRange.fromValues(7, 4, 7, 5), "x")); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/array-access.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Expressions.ArrayAccess", () => { 5 | it("can access multi-dimensional arrays", () => { 6 | verifyRuntimeResult(` 7 | x[0] = 1 8 | x[1][0] = 2 9 | x[3][5] = 3 10 | 11 | y = (x[1][0] + x[0]) * x[3][5] 12 | TextWindow.WriteLine(y)`, 13 | [], 14 | ["9"]); 15 | }); 16 | 17 | it("can access non-existent arrays", () => { 18 | verifyRuntimeResult(` 19 | TextWindow.WriteLine(x[0]) 20 | TextWindow.WriteLine(x[0][0]) 21 | TextWindow.WriteLine(x[2][3])`, 22 | [], 23 | [ 24 | "", 25 | "", 26 | "" 27 | ]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty": true, 4 | 5 | "strict": true, 6 | "allowJs": false, 7 | "alwaysStrict": true, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "strictNullChecks": true, 12 | "noImplicitReturns": true, 13 | "noUnusedParameters": true, 14 | "allowUnusedLabels": false, 15 | "allowUnreachableCode": false, 16 | "noFallthroughCasesInSwitch": true, 17 | 18 | "jsx": "react", 19 | "target": "es5", 20 | "module": "commonjs", 21 | "moduleResolution": "node", 22 | 23 | "sourceMap": true, 24 | "inlineSourceMap": false, 25 | "inlineSources": true, 26 | "noEmitOnError": true 27 | } 28 | } -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries/clock.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { Compilation } from "../../../../src/compiler/compilation"; 3 | import { ExecutionEngine, ExecutionMode, ExecutionState } from "../../../../src/compiler/execution-engine"; 4 | 5 | describe("Compiler.Runtime.Libraries.Clock", () => { 6 | it("can retreive current time", () => { 7 | const compilation = new Compilation(` 8 | x = Clock.Time`); 9 | 10 | const engine = new ExecutionEngine(compilation); 11 | engine.execute(ExecutionMode.RunToEnd); 12 | 13 | const value = engine.memory.values["x"]; 14 | expect(value.toValueString()).toMatch(/[0-9]{2}:[0-9]{2}:[0-9]{2}/); 15 | 16 | expect(engine.state).toBe(ExecutionState.Terminated); 17 | expect(engine.exception).toBeUndefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/clock.ts: -------------------------------------------------------------------------------- 1 | import { LibraryTypeInstance, LibraryMethodInstance, LibraryPropertyInstance, LibraryEventInstance } from "../libraries"; 2 | import { StringValue } from "../values/string-value"; 3 | import { BaseValue } from "../values/base-value"; 4 | 5 | export class ClockLibrary implements LibraryTypeInstance { 6 | private getTime(): BaseValue { 7 | const time = new Date().toLocaleTimeString(); 8 | return new StringValue(time); 9 | } 10 | 11 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = {}; 12 | 13 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = { 14 | Time: { getter: this.getTime.bind(this) } 15 | }; 16 | 17 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/services/hover-service.ts: -------------------------------------------------------------------------------- 1 | import { Compilation } from "../../../../../compiler/compilation"; 2 | import { HoverService } from "../../../../../compiler/services/hover-service"; 3 | import { EditorUtils } from "../../../../editor-utils"; 4 | 5 | export class EditorHoverService implements monaco.languages.HoverProvider { 6 | public provideHover(model: monaco.editor.IReadOnlyModel, position: monaco.Position): monaco.languages.Hover { 7 | const result = HoverService.provideHover(new Compilation(model.getValue()), EditorUtils.editorPositionToCompilerPosition(position)); 8 | if (result) { 9 | return { 10 | contents: result.text, 11 | range: EditorUtils.compilerRangeToEditorRange(result.range) 12 | }; 13 | } else { 14 | return null as any; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/text.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, ShapeKind } from "./base-shape"; 3 | 4 | export class TextShape extends BaseShape { 5 | public constructor(text: string) { 6 | super(ShapeKind.Text, new Konva.Text({ 7 | x: 0, 8 | y: 0, 9 | text: text, 10 | fontSize: 16, 11 | fontFamily: "calibri", 12 | fill: "slateblue" 13 | })); 14 | } 15 | 16 | public setText(text: string): void { 17 | this.instance.text(text); 18 | } 19 | 20 | public move(x: number, y: number): void { 21 | this.instance.x(x); 22 | this.instance.y(y); 23 | } 24 | 25 | public getLeft(): number { 26 | return this.instance.x(); 27 | } 28 | 29 | public getTop(): number { 30 | return this.instance.y(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/rectangle.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, strokeWidth, ShapeKind } from "./base-shape"; 3 | 4 | export class RectangleShape extends BaseShape { 5 | public constructor(width: number, height: number) { 6 | super(ShapeKind.Rectangle, new Konva.Rect({ 7 | x: strokeWidth / 2, 8 | y: strokeWidth / 2, 9 | width: width - strokeWidth, 10 | height: height - strokeWidth, 11 | fill: "slateBlue", 12 | stroke: "black", 13 | strokeWidth: strokeWidth 14 | })); 15 | } 16 | 17 | public getLeft(): number { 18 | return this.instance.x(); 19 | } 20 | 21 | public getTop(): number { 22 | return this.instance.y(); 23 | } 24 | 25 | public move(x: number, y: number): void { 26 | this.instance.x(x + strokeWidth / 2); 27 | this.instance.y(y + strokeWidth / 2); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/line.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, strokeWidth, ShapeKind } from "./base-shape"; 3 | 4 | export class LineShape extends BaseShape { 5 | public constructor(x1: number, y1: number, x2: number, y2: number) { 6 | const leftShift = Math.min(x1, x2); 7 | const topShift = Math.min(y1, y2); 8 | 9 | super(ShapeKind.Line, new Konva.Line({ 10 | points: [x1 - leftShift, y1 - topShift, x2 - leftShift, y2 - topShift], 11 | x: leftShift, 12 | y: topShift, 13 | stroke: "black", 14 | strokeWidth: strokeWidth 15 | })); 16 | } 17 | 18 | public getLeft(): number { 19 | return this.instance.x(); 20 | } 21 | 22 | public getTop(): number { 23 | return this.instance.y(); 24 | } 25 | 26 | public move(x: number, y: number): void { 27 | this.instance.x(x); 28 | this.instance.y(y); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/ellipse.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, strokeWidth, ShapeKind } from "./base-shape"; 3 | 4 | export class EllipseShape extends BaseShape { 5 | public constructor(width: number, height: number) { 6 | super(ShapeKind.Ellipse, new Konva.Ellipse({ 7 | x: width / 2, 8 | y: height / 2, 9 | radius: { 10 | x: (width - (2 * strokeWidth)) / 2, 11 | y: (height - (2 * strokeWidth)) / 2 12 | }, 13 | fill: "slateblue", 14 | stroke: "black", 15 | strokeWidth: strokeWidth 16 | })); 17 | } 18 | 19 | public getLeft(): number { 20 | return this.instance.x() - this.instance.radiusX(); 21 | } 22 | 23 | public getTop(): number { 24 | return this.instance.y() - this.instance.radiusY(); 25 | } 26 | 27 | public move(x: number, y: number): void { 28 | this.instance.y(y + this.instance.radiusY()); 29 | this.instance.x(x + this.instance.radiusX()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/compiler/utils/notifications.ts: -------------------------------------------------------------------------------- 1 | import * as PubSub from "pubsub-js"; 2 | 3 | // TODO: evaluate current usage and remove if not necessary 4 | 5 | export class PubSubChannel { 6 | private id: string; 7 | public constructor(name: string) { 8 | this.id = name + new Date().getTime().toString(); 9 | } 10 | 11 | public subscribe(subscriber: () => void): string { 12 | return PubSub.subscribe(this.id, subscriber); 13 | } 14 | 15 | public publish(): void { 16 | PubSub.publish(this.id, undefined); 17 | } 18 | } 19 | 20 | export class PubSubPayloadChannel { 21 | private id: string; 22 | public constructor(name: string) { 23 | this.id = name + new Date().getTime().toString(); 24 | } 25 | 26 | public subscribe(subscriber: (payload: TPayload) => void): string { 27 | return PubSub.subscribe(this.id, (_: string, payload: TPayload) => { 28 | subscriber(payload); 29 | }); 30 | } 31 | 32 | public publish(payload: TPayload): void { 33 | PubSub.publish(this.id, payload); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-consecutive-blank-lines": true, 4 | "eofline": true, 5 | "no-var-keyword": true, 6 | "triple-equals": true, 7 | "typedef": [ 8 | true, 9 | "call-signature", 10 | "parameter", 11 | "property-declaration", 12 | "member-variable-declaration", 13 | "object-destructuring", 14 | "array-destructuring" 15 | ], 16 | "member-access": [ 17 | true, 18 | "check-constructor", 19 | "check-accessor" 20 | ], 21 | "semicolon": [ 22 | true, 23 | "always" 24 | ], 25 | "quotemark": [ 26 | true, 27 | "double" 28 | ], 29 | "indent": [ 30 | true, 31 | "spaces", 32 | 4 33 | ], 34 | "trailing-comma": [ 35 | true, 36 | { 37 | "multiline": "never", 38 | "singleline": "never" 39 | } 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /src/app/components/common/modal/style.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | display: none; 3 | position: fixed; 4 | z-index: 1; 5 | padding-top: 100px; 6 | left: 0; 7 | top: 0; 8 | width: 100%; 9 | height: 100%; 10 | overflow: auto; 11 | background-color: rgba(0, 0, 0, 0.4); 12 | border: 1px solid black; 13 | box-shadow: 5px 5px 5px #888888; 14 | } 15 | 16 | .modal .content { 17 | background-color: #fefefe; 18 | margin: auto; 19 | padding: 20px; 20 | border: 1px solid #888; 21 | width: 80%; 22 | } 23 | 24 | .modal .close-button { 25 | color: #aaaaaa; 26 | float: right; 27 | font-size: 28px; 28 | font-weight: bold; 29 | text-decoration: none; 30 | cursor: pointer; 31 | } 32 | 33 | .modal .close-button:hover, 34 | .modal .close-button:focus { 35 | color: #000; 36 | } 37 | 38 | .modal .user-button { 39 | border: none; 40 | color: white; 41 | padding: 15px 32px; 42 | text-align: center; 43 | text-decoration: none; 44 | display: inline-block; 45 | font-size: 16px; 46 | cursor: pointer; 47 | margin-right: 5px; 48 | } -------------------------------------------------------------------------------- /src/compiler/syntax/tokens.ts: -------------------------------------------------------------------------------- 1 | import { CompilerRange } from "./ranges"; 2 | 3 | export enum TokenKind { 4 | UnrecognizedToken, 5 | 6 | IfKeyword, 7 | ThenKeyword, 8 | ElseKeyword, 9 | ElseIfKeyword, 10 | EndIfKeyword, 11 | ForKeyword, 12 | ToKeyword, 13 | StepKeyword, 14 | EndForKeyword, 15 | GoToKeyword, 16 | WhileKeyword, 17 | EndWhileKeyword, 18 | SubKeyword, 19 | EndSubKeyword, 20 | 21 | Dot, 22 | RightParen, 23 | LeftParen, 24 | RightSquareBracket, 25 | LeftSquareBracket, 26 | Comma, 27 | Equal, 28 | NotEqual, 29 | Plus, 30 | Minus, 31 | Multiply, 32 | Divide, 33 | Colon, 34 | LessThan, 35 | GreaterThan, 36 | LessThanOrEqual, 37 | GreaterThanOrEqual, 38 | Or, 39 | And, 40 | 41 | Identifier, 42 | NumberLiteral, 43 | StringLiteral, 44 | Comment 45 | } 46 | 47 | export class Token { 48 | public constructor( 49 | public readonly text: string, 50 | public readonly kind: TokenKind, 51 | public readonly range: CompilerRange) { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/triangle.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, strokeWidth, ShapeKind } from "./base-shape"; 3 | 4 | export class TriangleShape extends BaseShape { 5 | public constructor(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) { 6 | const leftShift = Math.min(x1, x2, x3); 7 | const topShift = Math.min(y1, y2, y3); 8 | 9 | super(ShapeKind.Triangle, new Konva.Line({ 10 | points: [x1 - leftShift, y1 - topShift, x2 - leftShift, y2 - topShift, x3 - leftShift, y3 - topShift], 11 | x: leftShift, 12 | y: topShift, 13 | fill: "slateblue", 14 | stroke: "black", 15 | strokeWidth: strokeWidth, 16 | closed: true 17 | })); 18 | } 19 | 20 | public getLeft(): number { 21 | return this.instance.x(); 22 | } 23 | 24 | public getTop(): number { 25 | return this.instance.y(); 26 | } 27 | 28 | public move(x: number, y: number): void { 29 | this.instance.x(x); 30 | this.instance.y(y); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Omar Tawfik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { EditorComponent } from "./components/editor"; 5 | import { RunComponent } from "./components/run"; 6 | import { DebugComponent } from "./components/debug"; 7 | import { reduce, createInitialState } from "./store"; 8 | import * as React from "react"; 9 | import { createStore } from "redux"; 10 | import * as ReactDOM from "react-dom"; 11 | import { HashRouter, Route, Switch, Redirect } from "react-router-dom"; 12 | import { Provider } from "react-redux"; 13 | 14 | window.document.title = "SmallBasic-Online"; 15 | 16 | ReactDOM.render(( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ), document.getElementById("react-app")); 28 | -------------------------------------------------------------------------------- /src/app/components/debug/memory/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ExecutionEngine } from "../../../../compiler/execution-engine"; 3 | import { EditorResources } from "../../../../strings/editor"; 4 | 5 | const MemoryIcon = require("./header.png"); 6 | 7 | interface MemoryProps { 8 | engine: ExecutionEngine; 9 | } 10 | 11 | interface MemoryState { 12 | } 13 | 14 | export class MemoryComponent extends React.Component { 15 | public render(): JSX.Element { 16 | return ( 17 |
18 |
19 |
{EditorResources.Memory_Header}
20 |
    21 | {Object.keys(this.props.engine.memory.values).sort().map((key, i) => 22 |
  • 23 | {key}: {this.props.engine.memory.values[key].toDebuggerString()} 24 |
  • 25 | )} 26 |
27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/debug/callstack/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ExecutionEngine } from "../../../../compiler/execution-engine"; 3 | import { EditorResources } from "../../../../strings/editor"; 4 | 5 | const CallStackIcon = require("./header.png"); 6 | 7 | interface CallStackProps { 8 | engine: ExecutionEngine; 9 | } 10 | 11 | interface CallStackState { 12 | } 13 | 14 | export class CallStackComponent extends React.Component { 15 | public render(): JSX.Element { 16 | return ( 17 |
18 |
19 |
{EditorResources.CallStack_Header}
20 |
    21 | {this.props.engine.executionStack.map((frame, i) => 22 |
  • 23 | {frame.moduleName}: ({this.props.engine.modules[frame.moduleName][frame.instructionIndex].sourceRange.start.line}) 24 |
  • 25 | )} 26 |
27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/compiler/runtime/stepping-through.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { Compilation } from "../../../src/compiler/compilation"; 3 | import { ExecutionEngine, ExecutionMode, ExecutionState } from "../../../src/compiler/execution-engine"; 4 | 5 | describe("Compiler.Runtime.SteppingThrough", () => { 6 | it("pauses when asked", () => { 7 | const compilation = new Compilation(` 8 | x = 1 9 | x = 2 10 | x = 3`); 11 | 12 | const engine = new ExecutionEngine(compilation); 13 | 14 | engine.execute(ExecutionMode.NextStatement); 15 | expect(engine.state).toBe(ExecutionState.Paused); 16 | expect(engine.memory.values["x"]).toBeUndefined(); 17 | 18 | engine.execute(ExecutionMode.NextStatement); 19 | expect(engine.state).toBe(ExecutionState.Paused); 20 | expect(engine.memory.values["x"].toDebuggerString()).toBe("1"); 21 | 22 | engine.execute(ExecutionMode.NextStatement); 23 | expect(engine.state).toBe(ExecutionState.Paused); 24 | expect(engine.memory.values["x"].toDebuggerString()).toBe("2"); 25 | 26 | engine.execute(ExecutionMode.NextStatement); 27 | expect(engine.state).toBe(ExecutionState.Terminated); 28 | expect(engine.memory.values["x"].toDebuggerString()).toBe("3"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries/program.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { Compilation } from "../../../../src/compiler/compilation"; 3 | import { ExecutionEngine, ExecutionMode, ExecutionState } from "../../../../src/compiler/execution-engine"; 4 | import { verifyRuntimeResult, TextWindowTestBuffer } from "../../helpers"; 5 | 6 | describe("Compiler.Runtime.Libraries.Program", () => { 7 | it("pauses when asked", () => { 8 | const compilation = new Compilation(` 9 | TextWindow.WriteLine("before") 10 | Program.Pause() 11 | TextWindow.WriteLine("after")`); 12 | 13 | const buffer = new TextWindowTestBuffer([], ["before", "after"]); 14 | const engine = new ExecutionEngine(compilation); 15 | 16 | engine.libraries.TextWindow.plugin = buffer; 17 | expect(buffer.outputIndex).toBe(0); 18 | 19 | engine.execute(ExecutionMode.Debug); 20 | expect(engine.state).toBe(ExecutionState.Paused); 21 | expect(buffer.outputIndex).toBe(1); 22 | 23 | engine.execute(ExecutionMode.Debug); 24 | expect(engine.state).toBe(ExecutionState.Terminated); 25 | expect(buffer.outputIndex).toBe(2); 26 | 27 | expect(engine.exception).toBeUndefined(); 28 | }); 29 | 30 | it("ends when asked", () => { 31 | verifyRuntimeResult(` 32 | For i = 1 To 5 33 | TextWindow.WriteLine(i) 34 | If i = 3 Then 35 | Program.End() 36 | EndIf 37 | EndFor`, 38 | [], 39 | ["1", "2", "3"]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/compiler/runtime/values/base-value.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEngine } from "../../execution-engine"; 2 | import { SubtractInstruction, AddInstruction, MultiplyInstruction, DivideInstruction } from "../../emitting/instructions"; 3 | 4 | export module Constants { 5 | export const True = "True"; 6 | export const False = "False"; 7 | } 8 | 9 | export enum ValueKind { 10 | String, 11 | Number, 12 | Array 13 | } 14 | 15 | // TODO: review all throws into a helper? 16 | 17 | export abstract class BaseValue { 18 | public abstract toBoolean(): boolean; 19 | public abstract toDebuggerString(): string; 20 | public abstract toValueString(): string; 21 | public abstract get kind(): ValueKind; 22 | 23 | // TODO: add another helper that just returns a number and review callers? 24 | public abstract tryConvertToNumber(): BaseValue; 25 | 26 | public abstract isEqualTo(other: BaseValue): boolean; 27 | public abstract isLessThan(other: BaseValue): boolean; 28 | public abstract isGreaterThan(other: BaseValue): boolean; 29 | 30 | public abstract add(other: BaseValue, engine: ExecutionEngine, instruction: AddInstruction): BaseValue; 31 | public abstract subtract(other: BaseValue, engine: ExecutionEngine, instruction: SubtractInstruction): BaseValue; 32 | public abstract multiply(other: BaseValue, engine: ExecutionEngine, instruction: MultiplyInstruction): BaseValue; 33 | public abstract divide(other: BaseValue, engine: ExecutionEngine, instruction: DivideInstruction): BaseValue; 34 | } 35 | -------------------------------------------------------------------------------- /tests/compiler/tests-list.ts: -------------------------------------------------------------------------------- 1 | import "./syntax/scanner"; 2 | import "./syntax/command-parser"; 3 | import "./syntax/statements-parser"; 4 | 5 | import "./binding/module-binder"; 6 | import "./binding/statement-binder"; 7 | import "./binding/expression-binder"; 8 | 9 | import "./services/completion-service"; 10 | import "./services/hover-service"; 11 | 12 | import "./runtime/expressions/addition"; 13 | import "./runtime/expressions/array-access"; 14 | import "./runtime/expressions/division"; 15 | import "./runtime/expressions/equality"; 16 | import "./runtime/expressions/greater-than-or-equal"; 17 | import "./runtime/expressions/greater-than"; 18 | import "./runtime/expressions/less-than-or-equal"; 19 | import "./runtime/expressions/less-than"; 20 | import "./runtime/expressions/logical-operators"; 21 | import "./runtime/expressions/multiplication"; 22 | import "./runtime/expressions/negation"; 23 | import "./runtime/expressions/subtraction"; 24 | import "./runtime/expressions/to-boolean"; 25 | 26 | import "./runtime/libraries/array"; 27 | import "./runtime/libraries/clock"; 28 | import "./runtime/libraries/math"; 29 | import "./runtime/libraries/program"; 30 | import "./runtime/libraries/stack"; 31 | import "./runtime/libraries/text-window"; 32 | 33 | import "./runtime/statements/for-loop"; 34 | import "./runtime/statements/if-statements"; 35 | import "./runtime/statements/labels"; 36 | import "./runtime/statements/while-loop"; 37 | 38 | import "./runtime/libraries-metadata"; 39 | import "./runtime/stepping-through"; 40 | import "./runtime/submodules"; 41 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/negation.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, ErrorCode } from "../../../../src/compiler/utils/diagnostics"; 2 | import "jasmine"; 3 | import { verifyRuntimeResult, verifyRuntimeError } from "../../helpers"; 4 | import { CompilerRange } from "../../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Runtime.Expressions.Negation", () => { 7 | it("can negate variables - numbers", () => { 8 | verifyRuntimeResult(` 9 | TextWindow.WriteLine(-2)`, 10 | [], 11 | ["-2"]); 12 | }); 13 | 14 | it("can negate variables - strings", () => { 15 | verifyRuntimeError(` 16 | x = "a" 17 | TextWindow.WriteLine(-x)`, 18 | // TextWindow.WriteLine(-x) 19 | // ^^ 20 | // You cannot use the operator '-' with a string value 21 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(2, 21, 2, 23), "-")); 22 | }); 23 | 24 | it("can negate variables - numeric strings", () => { 25 | verifyRuntimeResult(` 26 | x = "5" 27 | TextWindow.WriteLine(-x)`, 28 | [], 29 | ["-5"]); 30 | }); 31 | 32 | it("can negate variables - arrays", () => { 33 | verifyRuntimeError(` 34 | x[0] = 1 35 | TextWindow.WriteLine(-x)`, 36 | // TextWindow.WriteLine(-x) 37 | // ^^ 38 | // You cannot use the operator '-' with an array value 39 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 23), "-")); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/components/common/graphics-window/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Konva from "konva"; 3 | import { ExecutionEngine } from "../../../../compiler/execution-engine"; 4 | import { ShapesLibraryPlugin } from "../shapes/shapes-plugin"; 5 | 6 | import "./style.css"; 7 | 8 | interface GraphicsWindowComponentProps { 9 | engine: ExecutionEngine; 10 | } 11 | 12 | interface GraphicsWindowComponentState { 13 | stage?: Konva.Stage; 14 | layer?: Konva.Layer; 15 | } 16 | 17 | export class GraphicsWindowComponent extends React.Component { 18 | public constructor(props: GraphicsWindowComponentProps) { 19 | super(props); 20 | } 21 | 22 | public componentDidMount(): void { 23 | const stage = new Konva.Stage({ 24 | container: "graphics-container", 25 | width: document.getElementById("graphics-container")!.offsetWidth, 26 | height: document.getElementById("graphics-container")!.offsetHeight 27 | }); 28 | 29 | const layer = new Konva.Layer(); 30 | stage.add(layer); 31 | 32 | this.setState({ 33 | stage: stage, 34 | layer: layer 35 | }); 36 | 37 | const plugin = new ShapesLibraryPlugin(layer, stage); 38 | this.props.engine.libraries.Shapes.plugin = plugin; 39 | } 40 | 41 | public render(): JSX.Element { 42 | return ( 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/services/completion-service.ts: -------------------------------------------------------------------------------- 1 | import { CompletionService } from "../../../../../compiler/services/completion-service"; 2 | import { Compilation } from "../../../../../compiler/compilation"; 3 | import { EditorUtils } from "../../../../editor-utils"; 4 | 5 | export class EditorCompletionService implements monaco.languages.CompletionItemProvider { 6 | public readonly triggerCharacters: string[] = [ 7 | "." 8 | ]; 9 | 10 | public provideCompletionItems(model: monaco.editor.IReadOnlyModel, position: monaco.Position): monaco.languages.CompletionItem[] { 11 | return CompletionService.provideCompletion(new Compilation(model.getValue()), EditorUtils.editorPositionToCompilerPosition(position)).map(item => { 12 | let kind: monaco.languages.CompletionItemKind; 13 | switch (item.kind) { 14 | case CompletionService.ResultKind.Class: kind = monaco.languages.CompletionItemKind.Class; break; 15 | case CompletionService.ResultKind.Method: kind = monaco.languages.CompletionItemKind.Method; break; 16 | case CompletionService.ResultKind.Property: kind = monaco.languages.CompletionItemKind.Property; break; 17 | default: throw new Error(`Unrecognized CompletionService.CompletionItemKind '${CompletionService.ResultKind[item.kind]}'`); 18 | } 19 | 20 | return { 21 | label: item.title, 22 | kind: kind, 23 | detail: item.description, 24 | insertText: item.title 25 | }; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/compiler/runtime/statements/while-loop.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Statements.WhileLoop", () => { 5 | it("can execute as asked", () => { 6 | verifyRuntimeResult(` 7 | x = TextWindow.ReadNumber() 8 | result = 1 9 | While x > 0 10 | result = result * x 11 | x = x - 1 12 | EndWhile 13 | TextWindow.WriteLine(result)`, 14 | [4], 15 | ["24"]); 16 | }); 17 | 18 | it("never enters with a false condition", () => { 19 | verifyRuntimeResult(` 20 | x = TextWindow.ReadNumber() 21 | While x > 0 22 | TextWindow.WriteLine("entered") 23 | EndWhile`, 24 | [-5], 25 | []); 26 | }); 27 | 28 | it("breaks out of an infinite loop", () => { 29 | verifyRuntimeResult(` 30 | While "true" 31 | TextWindow.WriteLine("string? y/n") 32 | input = TextWindow.Read() 33 | If input = "y" Then 34 | TextWindow.WriteLine("string: " + TextWindow.Read()) 35 | ElseIf input = "n" Then 36 | TextWindow.WriteLine("number: " + TextWindow.ReadNumber()) 37 | Else 38 | Program.End() 39 | EndIf 40 | EndWhile`, 41 | [ 42 | "y", 43 | "test1", 44 | "n", 45 | 5, 46 | "y", 47 | "test3", 48 | "end" 49 | ], 50 | [ 51 | "string? y/n", 52 | "string: test1", 53 | "string? y/n", 54 | "number: 5", 55 | "string? y/n", 56 | "string: test3", 57 | "string? y/n" 58 | ]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as webpack from "webpack"; 3 | import * as HtmlWebpackPlugin from "html-webpack-plugin"; 4 | import { factory, parseEnvArguments } from "../../build/webpack.config"; 5 | import { Chunk } from "html-webpack-plugin"; 6 | 7 | export default function (env: any): webpack.Configuration { 8 | const parsedArgs = parseEnvArguments(env); 9 | 10 | const config = factory({ 11 | env: env, 12 | entryPath: { 13 | "monaco": "@timkendrick/monaco-editor/dist/standalone/index.js", 14 | "app": path.resolve(__dirname, "app.tsx") 15 | }, 16 | outputRelativePath: "app", 17 | target: "web" 18 | }); 19 | 20 | const minifyOptions = parsedArgs.release ? { 21 | caseSensitive: true, 22 | collapseWhitespace: true, 23 | conservativeCollapse: true, 24 | removeComments: true 25 | } : undefined; 26 | 27 | config.plugins!.push(new HtmlWebpackPlugin({ 28 | template: path.resolve(__dirname, "content/index.ejs"), 29 | minify: minifyOptions, 30 | hash: true, 31 | showErrors: false, 32 | favicon: path.resolve(__dirname, "content/appicon.png"), 33 | chunksSortMode: (a, b) => getChunkIndex(a) - getChunkIndex(b) 34 | })); 35 | 36 | return config; 37 | } 38 | 39 | function getChunkIndex(chunk: Chunk): number { 40 | if (chunk.names.length !== 1) { 41 | throw new Error(`Chunk '${JSON.stringify(chunk.names)}' should have exactly one name`); 42 | } 43 | 44 | const name = chunk.names[0]; 45 | 46 | switch (name) { 47 | case "monaco": return 0; 48 | case "app": return 1; 49 | default: throw new Error(`Chunk '${name}' does not have a predefined order`); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries/stack.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult, verifyRuntimeError } from "../../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Runtime.Libraries.Stack", () => { 7 | it("can push values and get counts", () => { 8 | verifyRuntimeResult(` 9 | TextWindow.WriteLine(Stack.GetCount("x")) 10 | TextWindow.WriteLine(Stack.GetCount("y")) 11 | Stack.PushValue("x", "first") 12 | TextWindow.WriteLine(Stack.GetCount("x")) 13 | TextWindow.WriteLine(Stack.GetCount("y")) 14 | Stack.PushValue("x", "second") 15 | Stack.PushValue("y", "other") 16 | TextWindow.WriteLine(Stack.GetCount("x")) 17 | TextWindow.WriteLine(Stack.GetCount("y"))`, 18 | [], 19 | [ 20 | "0", 21 | "0", 22 | "1", 23 | "0", 24 | "2", 25 | "1" 26 | ]); 27 | }); 28 | 29 | it("can push pop values in correct order", () => { 30 | verifyRuntimeResult(` 31 | Stack.PushValue("x", "first") 32 | Stack.PushValue("x", "second") 33 | TextWindow.WriteLine(Stack.PopValue("x")) 34 | TextWindow.WriteLine(Stack.PopValue("x"))`, 35 | [], 36 | [ 37 | "second", 38 | "first" 39 | ]); 40 | }); 41 | 42 | it("popping an empty stack produces an error", () => { 43 | verifyRuntimeError(` 44 | Stack.PopValue("x")`, 45 | // Stack.PopValue("x") 46 | // ^^^^^^^^^^^^^^^^^^^ 47 | // This stack has no elements to be popped 48 | new Diagnostic(ErrorCode.PoppingAnEmptyStack, CompilerRange.fromValues(1, 0, 1, 19))); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/app/components/common/toolbar-button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface ToolbarButtonProps { 4 | title: string; 5 | description: string; 6 | image: string; 7 | onClick?: React.EventHandler>; 8 | disabled?: boolean; 9 | } 10 | 11 | export class ToolbarButton extends React.Component { 12 | 13 | private onClick(e: React.MouseEvent): void { 14 | if (!this.props.disabled && this.props.onClick) { 15 | this.props.onClick(e); 16 | } 17 | } 18 | 19 | public render(): JSX.Element { 20 | const buttonStyle: React.CSSProperties = { 21 | position: "relative", 22 | backgroundImage: `url('${this.props.image}')`, 23 | height: "70px", 24 | width: "70px", 25 | minWidth: "50px", 26 | marginLeft: "5px", 27 | marginRight: "5px", 28 | backgroundPosition: "top", 29 | backgroundRepeat: "no-repeat", 30 | backgroundSize: "50px", 31 | cursor: "pointer" 32 | }; 33 | 34 | const labelStyle: React.CSSProperties = { 35 | height: "auto", 36 | width: "100%", 37 | textAlign: "center", 38 | margin: "0px", 39 | position: "absolute", 40 | bottom: 0, 41 | left: 0 42 | }; 43 | 44 | if (this.props.disabled) { 45 | buttonStyle.opacity = 0.5; 46 | buttonStyle.cursor = "not-allowed"; 47 | } 48 | 49 | return ( 50 |
51 |
{this.props.title}
52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/program.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionMode, ExecutionState, ExecutionEngine } from "../../execution-engine"; 2 | import { LibraryTypeInstance, LibraryPropertyInstance, LibraryMethodInstance, LibraryEventInstance } from "../libraries"; 3 | import { ValueKind } from "../values/base-value"; 4 | import { NumberValue } from "../values/number-value"; 5 | 6 | export class ProgramLibrary implements LibraryTypeInstance { 7 | private async executeDelay(engine: ExecutionEngine): Promise { 8 | const milliSecondsArg = engine.popEvaluationStack().tryConvertToNumber(); 9 | 10 | const milliSecondsValue = milliSecondsArg.kind === ValueKind.Number ? (milliSecondsArg as NumberValue).value : 0; 11 | 12 | const executionState = engine.state; 13 | engine.state = ExecutionState.BlockedOnInput; 14 | await new Promise(resolve => setTimeout(resolve, milliSecondsValue)); 15 | engine.state = executionState; 16 | } 17 | 18 | private executePause(engine: ExecutionEngine, mode: ExecutionMode): void { 19 | if (engine.state === ExecutionState.Paused) { 20 | engine.state = ExecutionState.Running; 21 | } else if (mode === ExecutionMode.Debug) { 22 | engine.state = ExecutionState.Paused; 23 | } 24 | } 25 | 26 | private executeEnd(engine: ExecutionEngine): void { 27 | engine.terminate(); 28 | } 29 | 30 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = { 31 | Delay: { execute: this.executeDelay.bind(this) }, 32 | Pause: { execute: this.executePause.bind(this) }, 33 | End: { execute: this.executeEnd.bind(this) } 34 | }; 35 | 36 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = {}; 37 | 38 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/common/modal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import "./style.css"; 4 | 5 | interface ModalProps { 6 | text: string; 7 | buttons: { 8 | text: string; 9 | color: string; 10 | }[]; 11 | onClick(button: number): void; 12 | } 13 | 14 | interface ModalState { 15 | hidden: boolean; 16 | } 17 | 18 | export class Modal extends React.Component { 19 | public constructor(props: ModalProps) { 20 | super(props); 21 | this.state = { 22 | hidden: true 23 | }; 24 | } 25 | 26 | public render(): JSX.Element { 27 | return ( 28 |
29 |
30 | × 31 |

{this.props.text}

32 |
33 | {this.props.buttons.map((button, i) => 34 |
this.onClick(i)}> 39 | {button.text} 40 |
41 | )} 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | public open(): void { 49 | this.setState({ 50 | hidden: false 51 | }); 52 | } 53 | 54 | public close(): void { 55 | this.setState({ 56 | hidden: true 57 | }); 58 | } 59 | 60 | private onClick(button: number): void { 61 | this.close(); 62 | this.props.onClick(button); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/compiler/runtime/statements/if-statements.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Statements.IfStatements", () => { 5 | const testCode1 = ` 6 | x = TextWindow.ReadNumber() 7 | If x = 1 Then 8 | TextWindow.WriteLine("one") 9 | ElseIf x = 2 Then 10 | TextWindow.WriteLine("two") 11 | ElseIf x = 3 Then 12 | TextWindow.WriteLine("three") 13 | Else 14 | TextWindow.WriteLine("none of the above") 15 | EndIf`; 16 | 17 | it("can evaluate if part", () => { 18 | verifyRuntimeResult(testCode1, [1], ["one"]); 19 | }); 20 | 21 | it("can evaluate first elseif part", () => { 22 | verifyRuntimeResult(testCode1, [2], ["two"]); 23 | }); 24 | 25 | it("can evaluate second elseif part", () => { 26 | verifyRuntimeResult(testCode1, [3], ["three"]); 27 | }); 28 | 29 | it("can evaluate else part", () => { 30 | verifyRuntimeResult(testCode1, [0], ["none of the above"]); 31 | }); 32 | 33 | const testCode2 = ` 34 | x = TextWindow.ReadNumber() 35 | If x = 1 or x = 2 Then 36 | TextWindow.WriteLine("first") 37 | ElseIf x <= 4 and x >= 3 Then 38 | TextWindow.WriteLine("second") 39 | Else 40 | TextWindow.WriteLine("third") 41 | EndIf`; 42 | 43 | it("can evaluate compound expressions - or - rhs", () => { 44 | verifyRuntimeResult(testCode2, [1], ["first"]); 45 | }); 46 | 47 | it("can evaluate compound expressions - or - lhs", () => { 48 | verifyRuntimeResult(testCode2, [2], ["first"]); 49 | }); 50 | 51 | it("can evaluate compound expressions - and - rhs", () => { 52 | verifyRuntimeResult(testCode2, [3], ["second"]); 53 | }); 54 | 55 | it("can evaluate compound expressions - and - lhs", () => { 56 | verifyRuntimeResult(testCode2, [4], ["second"]); 57 | }); 58 | 59 | it("can evaluate compound expressions - none", () => { 60 | verifyRuntimeResult(testCode2, [5], ["third"]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/compiler/binding/modules-binder.ts: -------------------------------------------------------------------------------- 1 | import { StatementBinder } from "./statement-binder"; 2 | import { Diagnostic, ErrorCode } from "../utils/diagnostics"; 3 | import { ParseTreeSyntax, StatementBlockSyntax } from "../syntax/syntax-nodes"; 4 | import { BoundStatementBlock } from "./bound-nodes"; 5 | 6 | export class ModulesBinder { 7 | public static readonly MainModuleName: string = "
"; 8 | 9 | private _definedSubModules: { [name: string]: boolean } = {}; 10 | private _boundModules: { [name: string]: BoundStatementBlock } = {}; 11 | 12 | public get boundModules(): { readonly [name: string]: BoundStatementBlock } { 13 | return this._boundModules; 14 | } 15 | 16 | public constructor( 17 | parseTree: ParseTreeSyntax, 18 | private readonly _diagnostics: Diagnostic[]) { 19 | this.constructSubModulesMap(parseTree); 20 | 21 | this._boundModules[ModulesBinder.MainModuleName] = this.bindModule(parseTree.mainModule); 22 | 23 | parseTree.subModules.forEach(subModule => { 24 | this._boundModules[subModule.subCommand.nameToken.token.text] = this.bindModule(subModule.statementsList); 25 | }); 26 | } 27 | 28 | private constructSubModulesMap(parseTree: ParseTreeSyntax): void { 29 | parseTree.subModules.forEach(subModule => { 30 | const nameToken = subModule.subCommand.nameToken; 31 | if (this._definedSubModules[nameToken.token.text]) { 32 | this._diagnostics.push(new Diagnostic( 33 | ErrorCode.TwoSubModulesWithTheSameName, 34 | nameToken.range, 35 | nameToken.token.text)); 36 | } else { 37 | this._definedSubModules[nameToken.token.text] = true; 38 | } 39 | }); 40 | } 41 | 42 | private bindModule(statements: StatementBlockSyntax): BoundStatementBlock { 43 | return new StatementBinder(statements, this._definedSubModules, this._diagnostics).result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/components/common/master-layout/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #74c1df; 3 | font-family: Consolas, monospace, Hack; 4 | } 5 | 6 | .body-box { 7 | border: 1px solid black; 8 | box-shadow: 5px 5px 5px #888888; 9 | background-color: #fafafc; 10 | padding: 10px; 11 | } 12 | 13 | .toolbar { 14 | height: auto; 15 | margin-bottom: 20px; 16 | min-width: 200px; 17 | display: flex; 18 | } 19 | 20 | .toolbar-buttons { 21 | width: 100%; 22 | display: flex; 23 | flex-flow: row wrap; 24 | } 25 | 26 | .toolbar-logo { 27 | float: right; 28 | cursor: pointer; 29 | min-width: 224px; 30 | min-height: 80px; 31 | background-size: 100%; 32 | background-repeat: no-repeat; 33 | background-image: url('../../../content/applogo.png'); 34 | } 35 | 36 | .full-container { 37 | display: flex; 38 | flex-flow: column; 39 | position: relative; 40 | height: 600px; 41 | } 42 | 43 | .master-container { 44 | display: flex; 45 | flex-flow: column; 46 | position: relative; 47 | float: left; 48 | width: calc(60% - 30px); 49 | height: 600px; 50 | } 51 | 52 | .sidebar-container { 53 | display: flex; 54 | flex-flow: column; 55 | position: relative; 56 | float: right; 57 | width: calc(40% - 30px); 58 | height: 600px; 59 | } 60 | 61 | .sidebar-component { 62 | flex: 1; 63 | padding: 10px; 64 | } 65 | 66 | .sidebar-component-icon { 67 | height: 50px; 68 | width: 50px; 69 | padding-right: 10px; 70 | padding-left: 10px; 71 | background-position: top; 72 | background-repeat: no-repeat; 73 | background-size: 50px; 74 | position: relative; 75 | float: left; 76 | } 77 | 78 | .sidebar-component-label { 79 | font-size: xx-large; 80 | font-weight: bold; 81 | line-height: 50px; 82 | margin-left: 10px; 83 | } 84 | 85 | .container-column { 86 | height: 100%; 87 | display: flex; 88 | justify-content: space-between; 89 | } 90 | 91 | .container-half-column { 92 | width: 50%; 93 | } -------------------------------------------------------------------------------- /src/compiler/syntax/ranges.ts: -------------------------------------------------------------------------------- 1 | export class CompilerPosition { 2 | public constructor( 3 | public readonly line: number, 4 | public readonly column: number) { 5 | } 6 | 7 | public equals(position: CompilerPosition): boolean { 8 | return this.line === position.line && this.column === position.column; 9 | } 10 | 11 | public before(position: CompilerPosition): boolean { 12 | if (this.line > position.line) return false; 13 | if (this.line < position.line) return true; 14 | return this.column < position.column; 15 | } 16 | 17 | public after(position: CompilerPosition): boolean { 18 | if (this.line < position.line) return false; 19 | if (this.line > position.line) return true; 20 | return this.column > position.column; 21 | } 22 | } 23 | 24 | export class CompilerRange { 25 | private constructor( 26 | public readonly start: CompilerPosition, 27 | public readonly end: CompilerPosition) { 28 | } 29 | 30 | public static fromValues(startLine: number, startColumn: number, endLine: number, endColumn: number): CompilerRange { 31 | return new CompilerRange( 32 | new CompilerPosition(startLine, startColumn), 33 | new CompilerPosition(endLine, endColumn)); 34 | } 35 | 36 | public static fromPositions(start: CompilerPosition, end: CompilerPosition): CompilerRange { 37 | return new CompilerRange(start, end); 38 | } 39 | 40 | public static combine(start: CompilerRange, end: CompilerRange): CompilerRange { 41 | return new CompilerRange(start.start, end.end); 42 | } 43 | 44 | public containsPosition(position: CompilerPosition): boolean { 45 | return (this.start.before(position) || this.start.equals(position)) 46 | && (position.before(this.end) || position.equals(this.end)); 47 | } 48 | 49 | public containsRange(range: CompilerRange): boolean { 50 | return this.containsPosition(range.start) && this.containsPosition(range.end); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/compiler/services/completion-service.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { CompletionService } from "../../../src/compiler/services/completion-service"; 3 | import { getMarkerPosition } from "../helpers"; 4 | import { Compilation } from "../../../src/compiler/compilation"; 5 | 6 | const marker = "$"; 7 | 8 | function testItemExists(text: string, expectedItem?: string): void { 9 | const position = getMarkerPosition(text, marker); 10 | const compilation = new Compilation(text.replace(marker, "")); 11 | const result = CompletionService.provideCompletion(compilation, position); 12 | 13 | if (expectedItem) { 14 | expect(result.filter(item => item.title === expectedItem).length).toBe(1); 15 | } else { 16 | expect(result.length).toBe(0); 17 | } 18 | } 19 | 20 | function testItemDoesNotExist(text: string, notExpectedItem: string): void { 21 | const position = getMarkerPosition(text, marker); 22 | const compilation = new Compilation(text.replace(marker, "")); 23 | const result = CompletionService.provideCompletion(compilation, position); 24 | 25 | expect(result.filter(item => item.title === notExpectedItem).length).toBe(0); 26 | } 27 | 28 | describe("Compiler.Services.CompletionService", () => { 29 | it("provides completion for a specific library", () => { 30 | testItemExists(`x = TextW${marker}`, "TextWindow"); 31 | }); 32 | 33 | it("does not provide completion for libraries not starting with the same prefix", () => { 34 | testItemDoesNotExist(`x = TextW${marker}`, "Time"); 35 | }); 36 | 37 | it("provides completion for a specific method", () => { 38 | testItemExists(`x = TextWindow.WriteL${marker}`, "WriteLine"); 39 | }); 40 | 41 | it("provides completion for a specific method - after dot", () => { 42 | testItemExists(`x = TextWindow.${marker}`, "WriteLine"); 43 | }); 44 | 45 | it("does not provide completion for methods not starting with the same prefix", () => { 46 | testItemDoesNotExist(`x = TextWindow.Wri${marker}`, "ForegroundColor"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/compiler/utils/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import { CompilerRange } from "../syntax/ranges"; 2 | import { CompilerUtils } from "./compiler-utils"; 3 | import { DiagnosticsResources } from "../../strings/diagnostics"; 4 | 5 | export enum ErrorCode { 6 | // Scanner Errors 7 | UnrecognizedCharacter, 8 | UnterminatedStringLiteral, 9 | 10 | // Line Parser Errors 11 | UnrecognizedCommand, 12 | UnexpectedToken_ExpectingExpression, 13 | UnexpectedToken_ExpectingToken, 14 | UnexpectedToken_ExpectingEOL, 15 | UnexpectedEOL_ExpectingExpression, 16 | UnexpectedEOL_ExpectingToken, 17 | 18 | // Tree Parser Errors 19 | UnexpectedCommand_ExpectingCommand, 20 | UnexpectedEOF_ExpectingCommand, 21 | CannotDefineASubInsideAnotherSub, 22 | CannotHaveCommandWithoutPreviousCommand, 23 | ValueIsNotANumber, 24 | 25 | // Binder Errors 26 | TwoSubModulesWithTheSameName, 27 | LabelDoesNotExist, 28 | UnassignedExpressionStatement, 29 | InvalidExpressionStatement, 30 | UnexpectedVoid_ExpectingValue, 31 | UnsupportedArrayBaseExpression, 32 | UnsupportedCallBaseExpression, 33 | UnexpectedArgumentsCount, 34 | PropertyHasNoSetter, 35 | UnsupportedDotBaseExpression, 36 | LibraryMemberNotFound, 37 | ValueIsNotAssignable, 38 | 39 | // Runtime Errors 40 | CannotUseAnArrayAsAnIndexToAnotherArray, 41 | CannotUseOperatorWithAnArray, 42 | CannotUseOperatorWithAString, 43 | CannotDivideByZero, 44 | PoppingAnEmptyStack 45 | } 46 | 47 | export class Diagnostic { 48 | public readonly args: ReadonlyArray; 49 | public constructor( 50 | public readonly code: ErrorCode, 51 | public readonly range: CompilerRange, 52 | ...args: string[]) { 53 | this.args = args; 54 | } 55 | 56 | public toString(): string { 57 | const template = DiagnosticsResources.get(ErrorCode[this.code]); 58 | 59 | if (!template) { 60 | throw new Error(`Error code ${ErrorCode[this.code]} has no string resource`); 61 | } 62 | 63 | return CompilerUtils.formatString(template, this.args); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/to-boolean.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Expressions.ToBoolean", () => { 5 | const numbersTestCode = ` 6 | x = TextWindow.ReadNumber() 7 | If x Then 8 | TextWindow.WriteLine("true") 9 | Else 10 | TextWindow.WriteLine("false") 11 | EndIf`; 12 | 13 | it("can convert variables to boolean - numbers - zero", () => { 14 | verifyRuntimeResult(numbersTestCode, [0], ["false"]); 15 | }); 16 | 17 | it("can convert variables to boolean - numbers - positive", () => { 18 | verifyRuntimeResult(numbersTestCode, [1], ["false"]); 19 | }); 20 | 21 | it("can convert variables to boolean - numbers - negative", () => { 22 | verifyRuntimeResult(numbersTestCode, [-1], ["false"]); 23 | }); 24 | 25 | const stringsTestCode = ` 26 | x = TextWindow.Read() 27 | If x Then 28 | TextWindow.WriteLine("true") 29 | Else 30 | TextWindow.WriteLine("false") 31 | EndIf`; 32 | 33 | it("can convert variables to boolean - strings - correct case True", () => { 34 | verifyRuntimeResult(stringsTestCode, ["True"], ["true"]); 35 | }); 36 | 37 | it("can convert variables to boolean - strings - upper case TRUE", () => { 38 | verifyRuntimeResult(stringsTestCode, ["TRUE"], ["true"]); 39 | }); 40 | 41 | it("can convert variables to boolean - strings - lower case true", () => { 42 | verifyRuntimeResult(stringsTestCode, ["true"], ["true"]); 43 | }); 44 | 45 | it("can convert variables to boolean - strings - false", () => { 46 | verifyRuntimeResult(stringsTestCode, ["False"], ["false"]); 47 | }); 48 | 49 | it("can convert variables to boolean - strings - anything", () => { 50 | verifyRuntimeResult(stringsTestCode, ["random string"], ["false"]); 51 | }); 52 | 53 | it("can convert variables to boolean - arrays", () => { 54 | verifyRuntimeResult(` 55 | x[0] = 1 56 | If x Then 57 | TextWindow.WriteLine("true") 58 | Else 59 | TextWindow.WriteLine("false") 60 | EndIf`, 61 | [], 62 | ["false"]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/strings/editor.ts: -------------------------------------------------------------------------------- 1 | // This file is generated through a build task. Do not edit by hand. 2 | 3 | export module EditorResources { 4 | export const ToolbarButton_New_Title = "New"; 5 | export const ToolbarButton_New_Description = "Clears the editor and starts a new program"; 6 | export const ToolbarButton_Publish_Title = "Publish"; 7 | export const ToolbarButton_Publish_Description = "Get a publish url to share with friends"; 8 | export const ToolbarButton_Cut_Title = "Cut"; 9 | export const ToolbarButton_Cut_Description = "Cut selected text"; 10 | export const ToolbarButton_Copy_Title = "Copy"; 11 | export const ToolbarButton_Copy_Description = "Copy selected text"; 12 | export const ToolbarButton_Paste_Title = "Paste"; 13 | export const ToolbarButton_Paste_Description = "Paste copied text"; 14 | export const ToolbarButton_Undo_Title = "Undo"; 15 | export const ToolbarButton_Undo_Description = "Undo last action"; 16 | export const ToolbarButton_Redo_Title = "Redo"; 17 | export const ToolbarButton_Redo_Description = "Redo last undone action"; 18 | export const ToolbarButton_Run_Title = "Run"; 19 | export const ToolbarButton_Run_Description = "Run this program"; 20 | export const ToolbarButton_Debug_Title = "Debug"; 21 | export const ToolbarButton_Debug_Description = "Debug this program"; 22 | export const ToolbarButton_Stop_Title = "Stop"; 23 | export const ToolbarButton_Stop_Description = "Stop execution of the program and return to the editor"; 24 | export const ToolbarButton_Step_Title = "Step"; 25 | export const ToolbarButton_Step_Description = "Step to the next statement of the program"; 26 | export const Modal_ConfirmNew_Text = "Are you sure you want to erase all code and start a new program?"; 27 | export const Modal_Button_Yes = "Yes"; 28 | export const Modal_Button_No = "No"; 29 | export const Documentation_Header = "Library"; 30 | export const CallStack_Header = "Call Stack"; 31 | export const Memory_Header = "Memory"; 32 | export const TextWindow_TerminationMessage = "Program ended..."; 33 | 34 | export function get(key: string): string { 35 | return (EditorResources)[key]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smallbasic-online", 3 | "title": "SmallBasic-Online", 4 | "version": "2.0.0", 5 | "private": true, 6 | "description": "An open-source IDE/runtime for the Small Basic programming language.", 7 | "author": "OmarTawfik", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sb/SmallBasic-Online.git" 12 | }, 13 | "scripts": { 14 | "gulp": "node node_modules/gulp/bin/gulp.js" 15 | }, 16 | "devDependencies": { 17 | "@timkendrick/monaco-editor": "0.0.7", 18 | "@types/applicationinsights-js": "1.0.5", 19 | "@types/detect-browser": "2.0.1", 20 | "@types/es6-promise": "0.0.33", 21 | "@types/gulp": "3.8.33", 22 | "@types/html-webpack-plugin": "2.28.0", 23 | "@types/jasmine": "2.6.0", 24 | "@types/pubsub-js": "1.5.18", 25 | "@types/react": "16.0.7", 26 | "@types/react-dom": "15.5.5", 27 | "@types/react-redux": "5.0.14", 28 | "@types/react-router-dom": "4.0.8", 29 | "@types/react-test-renderer": "15.5.4", 30 | "@types/redux": "3.6.0", 31 | "@types/rimraf": "2.0.2", 32 | "@types/source-map-support": "0.4.0", 33 | "@types/webpack": "3.0.12", 34 | "applicationinsights-js": "1.0.18", 35 | "awesome-typescript-loader": "3.2.3", 36 | "css-loader": "0.28.7", 37 | "detect-browser": "3.0.0", 38 | "electron": "1.8.4", 39 | "electron-builder": "20.15.1", 40 | "esutils": "2.0.2", 41 | "file-loader": "1.1.5", 42 | "gulp": "3.9.1", 43 | "html-webpack-plugin": "2.30.1", 44 | "jasmine": "2.8.0", 45 | "konva": "2.1.7", 46 | "monaco-editor": "0.10.1", 47 | "pubsub-js": "1.6.0", 48 | "react": "16.0.0", 49 | "react-dom": "16.0.0", 50 | "react-redux": "5.0.6", 51 | "react-router-dom": "4.2.2", 52 | "react-test-renderer": "16.0.0", 53 | "react-transition-group": "2.2.1", 54 | "redux": "3.7.2", 55 | "rimraf": "2.6.2", 56 | "source-map-loader": "0.2.1", 57 | "source-map-support": "0.5.0", 58 | "style-loader": "0.19.0", 59 | "ts-node": "3.3.0", 60 | "tslint": "5.7.0", 61 | "tslint-loader": "3.5.3", 62 | "typescript": "2.5.3", 63 | "webpack": "3.6.0", 64 | "webpack-dev-server": "2.9.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/stack.ts: -------------------------------------------------------------------------------- 1 | import { LibraryTypeInstance, LibraryMethodInstance, LibraryPropertyInstance, LibraryEventInstance } from "../libraries"; 2 | import { ExecutionEngine, ExecutionMode } from "../../execution-engine"; 3 | import { BaseValue } from "../values/base-value"; 4 | import { NumberValue } from "../values/number-value"; 5 | import { Diagnostic, ErrorCode } from "../../utils/diagnostics"; 6 | import { CompilerRange } from "../../syntax/ranges"; 7 | 8 | export class StackLibrary implements LibraryTypeInstance { 9 | private _stacks: { [name: string]: BaseValue[] } = {}; 10 | 11 | private executePushValue(engine: ExecutionEngine): void { 12 | const value = engine.popEvaluationStack(); 13 | const stackName = engine.popEvaluationStack().toValueString(); 14 | 15 | if (!this._stacks[stackName]) { 16 | this._stacks[stackName] = []; 17 | } 18 | 19 | this._stacks[stackName].push(value); 20 | } 21 | 22 | private executeGetCount(engine: ExecutionEngine): void { 23 | const stackName = engine.popEvaluationStack().toValueString(); 24 | const count = this._stacks[stackName] ? this._stacks[stackName].length : 0; 25 | 26 | engine.pushEvaluationStack(new NumberValue(count)); 27 | } 28 | 29 | private executePopValue(engine: ExecutionEngine, _: ExecutionMode, range: CompilerRange): void { 30 | const stackName = engine.popEvaluationStack().toValueString(); 31 | 32 | if (this._stacks[stackName] && this._stacks[stackName].length) { 33 | engine.pushEvaluationStack(this._stacks[stackName].pop()!); 34 | } else { 35 | engine.terminate(new Diagnostic(ErrorCode.PoppingAnEmptyStack, range)); 36 | } 37 | } 38 | 39 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = { 40 | PushValue: { execute: this.executePushValue.bind(this) }, 41 | GetCount: { execute: this.executeGetCount.bind(this) }, 42 | PopValue: { execute: this.executePopValue.bind(this) } 43 | }; 44 | 45 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = {}; 46 | 47 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 48 | } 49 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEngine, ExecutionMode } from "../execution-engine"; 2 | import { TextWindowLibrary } from "./libraries/text-window"; 3 | import { ProgramLibrary } from "./libraries/program"; 4 | import { ClockLibrary } from "./libraries/clock"; 5 | import { ArrayLibrary } from "./libraries/array"; 6 | import { StackLibrary } from "./libraries/stack"; 7 | import { BaseValue } from "./values/base-value"; 8 | import { LibrariesMetadata } from "./libraries-metadata"; 9 | import { MathLibrary } from "./libraries/math"; 10 | import { CompilerRange } from "../syntax/ranges"; 11 | import { ShapesLibrary } from "./libraries/shapes"; 12 | 13 | export interface LibraryTypeInstance { 14 | readonly methods: { readonly [name: string]: LibraryMethodInstance }; 15 | readonly properties: { readonly [name: string]: LibraryPropertyInstance }; 16 | readonly events: { readonly [name: string]: LibraryEventInstance }; 17 | } 18 | 19 | export interface LibraryMethodInstance { 20 | readonly execute: (engine: ExecutionEngine, mode: ExecutionMode, range: CompilerRange) => void; 21 | } 22 | 23 | export interface LibraryPropertyInstance { 24 | readonly getter?: () => BaseValue; 25 | readonly setter?: (value: BaseValue) => void; 26 | } 27 | 28 | export interface LibraryEventInstance { 29 | readonly setSubModule: (name: string) => void; 30 | readonly raise: (engine: ExecutionEngine) => void; 31 | } 32 | 33 | export class RuntimeLibraries { 34 | readonly [libraryName: string]: LibraryTypeInstance; 35 | 36 | public static readonly Metadata: LibrariesMetadata = new LibrariesMetadata(); 37 | 38 | public readonly Array: ArrayLibrary = new ArrayLibrary(); 39 | public readonly Clock: ClockLibrary = new ClockLibrary(); 40 | // TODO: public readonly Controls: ControlsLibrary = new ControlsLibrary(); 41 | public readonly Math: MathLibrary = new MathLibrary(); 42 | public readonly Program: ProgramLibrary = new ProgramLibrary(); 43 | public readonly Shapes: ShapesLibrary = new ShapesLibrary(); 44 | public readonly Stack: StackLibrary = new StackLibrary(); 45 | public readonly TextWindow: TextWindowLibrary = new TextWindowLibrary(); 46 | // TODO: public readonly Turtle: TurtleLibrary = new TurtleLibrary(); 47 | } 48 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/logical-operators.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Expressions.LogicalOperators", () => { 5 | it("computes false and false", () => { 6 | verifyRuntimeResult(` 7 | If "false" and "false" Then 8 | TextWindow.WriteLine("true") 9 | Else 10 | TextWindow.WriteLine("false") 11 | EndIf`, 12 | [], 13 | ["false"]); 14 | }); 15 | 16 | it("computes false and true", () => { 17 | verifyRuntimeResult(` 18 | If "false" and "true" Then 19 | TextWindow.WriteLine("true") 20 | Else 21 | TextWindow.WriteLine("false") 22 | EndIf`, 23 | [], 24 | ["false"]); 25 | }); 26 | 27 | it("computes true and false", () => { 28 | verifyRuntimeResult(` 29 | If "true" and "false" Then 30 | TextWindow.WriteLine("true") 31 | Else 32 | TextWindow.WriteLine("false") 33 | EndIf`, 34 | [], 35 | ["false"]); 36 | }); 37 | 38 | it("computes true and true", () => { 39 | verifyRuntimeResult(` 40 | If "true" and "true" Then 41 | TextWindow.WriteLine("true") 42 | Else 43 | TextWindow.WriteLine("false") 44 | EndIf`, 45 | [], 46 | ["true"]); 47 | }); 48 | 49 | it("computes false or false", () => { 50 | verifyRuntimeResult(` 51 | If "false" or "false" Then 52 | TextWindow.WriteLine("true") 53 | Else 54 | TextWindow.WriteLine("false") 55 | EndIf`, 56 | [], 57 | ["false"]); 58 | }); 59 | 60 | it("computes false or true", () => { 61 | verifyRuntimeResult(` 62 | If "false" or "true" Then 63 | TextWindow.WriteLine("true") 64 | Else 65 | TextWindow.WriteLine("false") 66 | EndIf`, 67 | [], 68 | ["true"]); 69 | }); 70 | 71 | it("computes true or false", () => { 72 | verifyRuntimeResult(` 73 | If "true" or "false" Then 74 | TextWindow.WriteLine("true") 75 | Else 76 | TextWindow.WriteLine("false") 77 | EndIf`, 78 | [], 79 | ["true"]); 80 | }); 81 | 82 | it("computes true or true", () => { 83 | verifyRuntimeResult(` 84 | If "true" or "true" Then 85 | TextWindow.WriteLine("true") 86 | Else 87 | TextWindow.WriteLine("false") 88 | EndIf`, 89 | [], 90 | ["true"]); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/compiler/runtime/statements/for-loop.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Statements.ForLoop", () => { 5 | it("can execute without a step condition", () => { 6 | verifyRuntimeResult(` 7 | For i = 1 To 5 8 | TextWindow.WriteLine(i) 9 | EndFor`, 10 | [], 11 | ["1", "2", "3", "4", "5"]); 12 | }); 13 | 14 | it("can execute without a step condition - inverse", () => { 15 | verifyRuntimeResult(` 16 | For i = 5 To 1 17 | TextWindow.WriteLine(i) 18 | EndFor`, 19 | [], 20 | []); 21 | }); 22 | 23 | it("can execute with a step condition - one", () => { 24 | verifyRuntimeResult(` 25 | For i = 1 To 5 Step 1 26 | TextWindow.WriteLine(i) 27 | EndFor`, 28 | [], 29 | ["1", "2", "3", "4", "5"]); 30 | }); 31 | 32 | it("can execute with a step condition - two", () => { 33 | verifyRuntimeResult(` 34 | For i = 1 To 10 Step 2 35 | TextWindow.WriteLine(i) 36 | EndFor`, 37 | [], 38 | ["1", "3", "5", "7", "9"]); 39 | }); 40 | 41 | it("can execute with an inverse step condition - one", () => { 42 | verifyRuntimeResult(` 43 | For i = 5 To 1 Step -1 44 | TextWindow.WriteLine(i) 45 | EndFor`, 46 | [], 47 | ["5", "4", "3", "2", "1"]); 48 | }); 49 | 50 | it("can execute with an inverse step condition - two", () => { 51 | verifyRuntimeResult(` 52 | For i = 10 To 1 Step -2 53 | TextWindow.WriteLine(i) 54 | EndFor`, 55 | [], 56 | ["10", "8", "6", "4", "2"]); 57 | }); 58 | 59 | it("can execute with an equal start and end - no step", () => { 60 | verifyRuntimeResult(` 61 | For i = 1 To 1 62 | TextWindow.WriteLine(i) 63 | EndFor`, 64 | [], 65 | ["1"]); 66 | }); 67 | 68 | it("can execute with an equal start and end - positive step", () => { 69 | verifyRuntimeResult(` 70 | For i = 1 To 1 Step 1 71 | TextWindow.WriteLine(i) 72 | EndFor`, 73 | [], 74 | ["1"]); 75 | }); 76 | 77 | it("can execute with an equal start and end - negative step", () => { 78 | verifyRuntimeResult(` 79 | For i = 1 To 1 Step -1 80 | TextWindow.WriteLine(i) 81 | EndFor`, 82 | [], 83 | ["1"]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { Compilation } from "../compiler/compilation"; 2 | import { Action } from "redux"; 3 | import { AppInsights } from "applicationinsights-js"; 4 | 5 | export enum ActionType { 6 | SetText 7 | } 8 | 9 | export interface BaseAction extends Action { 10 | readonly type: ActionType; 11 | } 12 | 13 | export interface SetTextAction extends BaseAction { 14 | text: string; 15 | } 16 | 17 | export class ActionFactory { 18 | private constructor() { 19 | } 20 | 21 | public static setText(text: string): SetTextAction { 22 | return { 23 | type: ActionType.SetText, 24 | text: text 25 | }; 26 | } 27 | } 28 | 29 | export interface AppState { 30 | readonly compilation: Compilation; 31 | readonly appInsights: Microsoft.ApplicationInsights.IAppInsights; 32 | } 33 | 34 | export function reduce(state: AppState | undefined, action: BaseAction): AppState { 35 | if (!state) { 36 | return createInitialState(); 37 | } 38 | 39 | switch (action.type) { 40 | case ActionType.SetText: { 41 | return { 42 | compilation: new Compilation((action as SetTextAction).text), 43 | appInsights: state.appInsights 44 | }; 45 | } 46 | 47 | default: { 48 | return state; 49 | } 50 | } 51 | } 52 | 53 | export function createInitialState(): AppState { 54 | AppInsights.downloadAndSetup!({ 55 | // TODO: build script needs to fix these two values: 56 | disableTelemetry: true, 57 | instrumentationKey: "xxxx-xxx-xxx-xxx-xxxxxxx", 58 | 59 | verboseLogging: true, 60 | disableExceptionTracking: false, 61 | disableAjaxTracking: false 62 | }); 63 | 64 | appInsights.queue.push(() => { 65 | appInsights.context.addTelemetryInitializer(envelope => { 66 | const telemetryItem = envelope.data.baseData; 67 | telemetryItem.properties = telemetryItem.properties || {}; 68 | 69 | // Add these properties to all logs: 70 | telemetryItem.properties.urlReferrer = document.referrer; 71 | }); 72 | }); 73 | 74 | return { 75 | compilation: new Compilation(` 76 | ' A new Program! 77 | TextWindow.WriteLine("What is your name?") 78 | name = TextWindow.Read() 79 | TextWindow.WriteLine("Hello " + name + "!")`), 80 | appInsights: appInsights 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/compiler/emitting/temp-labels-remover.ts: -------------------------------------------------------------------------------- 1 | import { BaseInstruction, InstructionKind, TempLabelInstruction, TempJumpInstruction, JumpInstruction, TempConditionalJumpInstruction, ConditionalJumpInstruction } from "./instructions"; 2 | 3 | export module TempLabelsRemover { 4 | export function remove(instructions: BaseInstruction[]): void { 5 | const map: { [key: string]: number } = {}; 6 | 7 | for (let i = 0; i < instructions.length; i++) { 8 | if (instructions[i].kind === InstructionKind.TempLabel) { 9 | const label = instructions[i] as TempLabelInstruction; 10 | if (map[label.name]) { 11 | throw new Error(`Label '${label.name}' exists twice in the same instruction set at '${map[label.name]}' and '${i}'`); 12 | } 13 | map[label.name] = i; 14 | instructions.splice(i, 1); 15 | i--; 16 | } 17 | } 18 | 19 | for (let i = 0; i < instructions.length; i++) { 20 | switch (instructions[i].kind) { 21 | case InstructionKind.TempJump: { 22 | const jump = instructions[i] as TempJumpInstruction; 23 | instructions[i] = new JumpInstruction(replaceJump(jump.target, map)!, jump.sourceRange); 24 | break; 25 | } 26 | case InstructionKind.TempConditionalJump: { 27 | const jump = instructions[i] as TempConditionalJumpInstruction; 28 | instructions[i] = new ConditionalJumpInstruction(replaceJump(jump.trueTarget, map), replaceJump(jump.falseTarget, map), jump.sourceRange); 29 | break; 30 | } 31 | case InstructionKind.Jump: 32 | case InstructionKind.ConditionalJump: { 33 | throw new Error(`Unexpected instruction kind: ${InstructionKind[instructions[i].kind]}`); 34 | } 35 | } 36 | } 37 | } 38 | 39 | function replaceJump(target: string | undefined, map: { [key: string]: number }): number | undefined { 40 | if (target) { 41 | const index = map[target]; 42 | if (index === undefined) { 43 | throw new Error(`Index for label ${target} was not calculated`); 44 | } else { 45 | return index; 46 | } 47 | } else { 48 | return undefined; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/editor-utils.ts: -------------------------------------------------------------------------------- 1 | import { CompilerRange, CompilerPosition } from "../compiler/syntax/ranges"; 2 | import { TextWindowColor } from "../compiler/runtime/libraries/text-window"; 3 | 4 | export module EditorUtils { 5 | export function compilerPositionToEditorPosition(position: CompilerPosition): monaco.Position { 6 | return new monaco.Position(position.line + 1, position.column + 1); 7 | } 8 | 9 | export function editorPositionToCompilerPosition(position: monaco.Position): CompilerPosition { 10 | return new CompilerPosition(position.lineNumber - 1, position.column - 1); 11 | } 12 | 13 | export function compilerRangeToEditorRange(range: CompilerRange): monaco.Range { 14 | return monaco.Range.fromPositions( 15 | EditorUtils.compilerPositionToEditorPosition(range.start), 16 | EditorUtils.compilerPositionToEditorPosition(range.end)); 17 | } 18 | 19 | export function editorRangeToCompilerRange(range: monaco.Range): CompilerRange { 20 | return CompilerRange.fromPositions( 21 | EditorUtils.editorPositionToCompilerPosition(range.getStartPosition()), 22 | EditorUtils.editorPositionToCompilerPosition(range.getEndPosition())); 23 | } 24 | 25 | export function textWindowColorToCssColor(color: TextWindowColor): string { 26 | switch (color) { 27 | case TextWindowColor.Black: return "rgb(0, 0, 0)"; 28 | case TextWindowColor.DarkBlue: return "rgb(0, 0, 128)"; 29 | case TextWindowColor.DarkGreen: return "rgb(0, 128, 0)"; 30 | case TextWindowColor.DarkCyan: return "rgb(0, 128, 128)"; 31 | case TextWindowColor.DarkRed: return "rgb(128, 0, 0)"; 32 | case TextWindowColor.DarkMagenta: return "rgb(128, 0, 128)"; 33 | case TextWindowColor.DarkYellow: return "rgb(128, 128, 0)"; 34 | case TextWindowColor.Gray: return "rgb(128, 128, 128)"; 35 | case TextWindowColor.DarkGray: return "rgb(64, 64, 64)"; 36 | case TextWindowColor.Blue: return "rgb(0, 0, 255)"; 37 | case TextWindowColor.Green: return "rgb(0, 255, 0)"; 38 | case TextWindowColor.Cyan: return "rgb(0, 255, 255)"; 39 | case TextWindowColor.Red: return "rgb(255, 0, 0)"; 40 | case TextWindowColor.Magenta: return "rgb(255, 0, 255)"; 41 | case TextWindowColor.Yellow: return "rgb(255, 255, 0)"; 42 | case TextWindowColor.White: return "rgb(255, 255, 255)"; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/components/common/master-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import "./style.css"; 4 | import { RouteComponentProps, withRouter } from "react-router"; 5 | import { AppState } from "../../../store"; 6 | import { Dispatch, connect } from "react-redux"; 7 | 8 | interface PropsFromState { 9 | } 10 | 11 | interface PropsFromDispatch { 12 | } 13 | 14 | interface PropsFromReact { 15 | toolbar: JSX.Element[]; 16 | masterContainer: JSX.Element; 17 | sideBar?: JSX.Element; 18 | } 19 | 20 | type PresentationalComponentProps = PropsFromState & PropsFromDispatch & PropsFromReact & RouteComponentProps; 21 | 22 | class PresentationalComponent extends React.Component { 23 | public constructor(props: PresentationalComponentProps) { 24 | super(props); 25 | } 26 | 27 | public render(): JSX.Element { 28 | return ( 29 |
30 |
31 |
32 | {this.props.toolbar.map((item, i) =>
{item}
)} 33 |
34 |
35 |
36 | 37 | {this.props.sideBar ? 38 |
39 |
40 | {this.props.masterContainer} 41 |
42 | 43 |
44 | {this.props.sideBar} 45 |
46 |
47 | : 48 |
49 | {this.props.masterContainer} 50 |
51 | } 52 |
53 | ); 54 | } 55 | 56 | private onLogoClick(): void { 57 | if (this.props.location.pathname !== "/editor") { 58 | this.props.history.push("/editor"); 59 | } 60 | } 61 | } 62 | 63 | function mapStateToProps(_: AppState): PropsFromState { 64 | return { 65 | }; 66 | } 67 | 68 | function mapDispatchToProps(_: Dispatch): PropsFromDispatch { 69 | return { 70 | }; 71 | } 72 | 73 | export const MasterLayoutComponent = connect(mapStateToProps, mapDispatchToProps)(withRouter(PresentationalComponent as any)); 74 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/styles/monaco-override.css: -------------------------------------------------------------------------------- 1 | .new-style .monaco-list>.monaco-scrollable-element { 2 | background: rgb(224, 237, 243) url("../images/intellisense-background.png") left bottom no-repeat; 3 | } 4 | 5 | .new-style .monaco-editor .suggest-widget>.tree, 6 | .new-style .monaco-editor .suggest-widget>.tree .monaco-list, 7 | .new-style .monaco-editor .suggest-widget>.tree .monaco-list .monaco-list-rows, 8 | .new-style .monaco-list>.monaco-scrollable-element { 9 | border-radius: 8px; 10 | } 11 | 12 | .new-style .monaco-editor .suggest-widget>.tree, 13 | .new-style .monaco-editor .suggest-widget>.tree .monaco-list .monaco-list-rows { 14 | height: 170px !important; 15 | } 16 | 17 | .new-style .monaco-list.list_id_1 .monaco-list-row:hover, 18 | .new-style .monaco-list.list_id_1 .monaco-list-row:focus, 19 | .new-style .monaco-list.list_id_1 .monaco-list-row.focused { 20 | background-color: unset !important; 21 | } 22 | 23 | .new-style .monaco-editor .suggest-widget>.tree .monaco-list .monaco-list-rows { 24 | top: unset !important; 25 | } 26 | 27 | .new-style .monaco-editor .suggest-widget .monaco-list .monaco-list-row { 28 | position: initial; 29 | } 30 | 31 | .new-style .monaco-list.list_id_1 .monaco-list-row .monaco-highlighted-label { 32 | font-size: 16px; 33 | } 34 | 35 | .new-style .monaco-list.list_id_1 .monaco-list-row.focused .monaco-highlighted-label { 36 | font-weight: bolder; 37 | } 38 | 39 | .new-style .monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.type-label { 40 | width: 248px; 41 | height: 150px; 42 | margin-left: 150px; 43 | font-size: .8em; 44 | line-height: 1.3em; 45 | border-radius: 8px; 46 | flex: unset; 47 | text-align: left; 48 | display: block; 49 | overflow-y: auto; 50 | overflow-x: hidden; 51 | position: absolute; 52 | text-overflow: initial; 53 | white-space: normal; 54 | top: 5px; 55 | } 56 | 57 | .new-style .monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.readMore { 58 | display: none; 59 | } 60 | 61 | .new-style .monaco-editor .suggest-widget.docs-below .details { 62 | border-top-width: 0px; 63 | position: absolute; 64 | top: 0; 65 | right: 0; 66 | width: 260px; 67 | min-height: 158px; 68 | display: block; 69 | } 70 | 71 | .new-style .monaco-scrollable-element>.scrollbar, 72 | .new-style .monaco-scrollable-element>.vertical, 73 | .new-style .monaco-scrollable-element>.invisible { 74 | height: 144px !important; 75 | } 76 | 77 | .new-style .vs .monaco-scrollable-element>.scrollbar>.slider { 78 | display: none; 79 | } -------------------------------------------------------------------------------- /tests/compiler/services/hover-service.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { DiagnosticsResources } from "../../../src/strings/diagnostics"; 3 | import { CompilerUtils } from "../../../src/compiler/utils/compiler-utils"; 4 | import { getMarkerPosition } from "../helpers"; 5 | import { HoverService } from "../../../src/compiler/services/hover-service"; 6 | import { DocumentationResources } from "../../../src/strings/documentation"; 7 | import { Compilation } from "../../../src/compiler/compilation"; 8 | 9 | const marker = "$"; 10 | 11 | function testHover(text: string, expectedHover?: string[]): void { 12 | const position = getMarkerPosition(text, marker); 13 | const compilation = new Compilation(text.replace(marker, "")); 14 | const result = HoverService.provideHover(compilation, position); 15 | 16 | if (expectedHover) { 17 | expect(result).toBeDefined(); 18 | expect(result!.range.containsPosition(position)).toBeTruthy(); 19 | expect(result!.text.length).toBe(expectedHover.length); 20 | for (let i = 0; i < expectedHover.length; i++) { 21 | expect(result!.text[i]).toBe(expectedHover[i]); 22 | } 23 | } else { 24 | expect(result).toBeUndefined(); 25 | } 26 | } 27 | 28 | describe("Compiler.Services.HoverService", () => { 29 | it("provides error description on hover - lhs", () => { 30 | testHover(` 31 | Text${marker}Window[0] = 5`, [ 32 | DiagnosticsResources.UnexpectedVoid_ExpectingValue 33 | ]); 34 | }); 35 | 36 | it("provides error description on hover - rhs", () => { 37 | testHover(` 38 | x = TextWindow.Write${marker}Line()`, [ 39 | CompilerUtils.formatString(DiagnosticsResources.UnexpectedArgumentsCount, ["1", "0"]) 40 | ]); 41 | }); 42 | 43 | it("provides library method name when hovered over - statement", () => { 44 | testHover(` 45 | TextWindow.Write${marker}Line("")`, [ 46 | "TextWindow.WriteLine", 47 | DocumentationResources.TextWindow_WriteLine 48 | ]); 49 | }); 50 | 51 | it("provides library method name when hovered over - expression", () => { 52 | testHover(` 53 | x = TextWindow.Rea${marker}d()`, [ 54 | "TextWindow.Read", 55 | DocumentationResources.TextWindow_Read 56 | ]); 57 | }); 58 | 59 | it("provides library property name when hovered over", () => { 60 | testHover(` 61 | x = TextWindow.Foreground${marker}Color`, [ 62 | "TextWindow.ForegroundColor", 63 | DocumentationResources.TextWindow_ForegroundColor 64 | ]); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/base-shape.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | 3 | // TODO: 4 | // Need to factor strokewidth into the size of the shape to have 5 | // parity with the desktop version. Konva counts the stroke width separately. 6 | // Ex: to achieve a total width of 20, radius is (20 - (2*2))/2 = 16/2 = 8 pix. 7 | // Then 8 pix + 2 pix is 10 pix which is half the desired width. 8 | export const strokeWidth: number = 2; 9 | 10 | export enum ShapeKind { 11 | Ellipse, 12 | Line, 13 | Rectangle, 14 | Text, 15 | Triangle 16 | } 17 | 18 | let nameGenerationCounter: number = 0; 19 | 20 | export abstract class BaseShape { 21 | 22 | public readonly name: string; 23 | 24 | public constructor( 25 | public readonly kind: ShapeKind, 26 | public readonly instance: TShape) { 27 | nameGenerationCounter++; 28 | 29 | this.name = ShapeKind[this.kind] + nameGenerationCounter.toString(); 30 | } 31 | 32 | public abstract getLeft(): number; 33 | public abstract getTop(): number; 34 | public abstract move(x: number, y: number): void; 35 | 36 | public getOpacity(): number { 37 | return this.instance.opacity(); 38 | } 39 | 40 | public setOpacity(opacity: number): void { 41 | this.instance.opacity(opacity); 42 | } 43 | 44 | public hideShape(): void { 45 | this.instance.hide(); 46 | } 47 | 48 | public showShape(): void { 49 | this.instance.show(); 50 | } 51 | 52 | public zoom(scaleX: number, scaleY: number): void { 53 | this.instance.scale({ 54 | x: scaleX, 55 | y: scaleY 56 | }); 57 | } 58 | 59 | public remove(): void { 60 | this.instance.destroy(); 61 | } 62 | 63 | public rotate(degrees: number): void { 64 | this.instance.rotation(degrees); 65 | } 66 | 67 | public animate(x: number, y: number, durationInMilliseconds: number, layer: Konva.Layer): void { 68 | const self = this; 69 | const startX = this.getLeft(); 70 | const startY = this.getTop(); 71 | const xDistanceToMove = x - this.getLeft(); 72 | const yDistanceToMove = y - this.getTop(); 73 | 74 | const animation = new Konva.Animation((frame: any) => { 75 | const percentage = frame.time / durationInMilliseconds; 76 | const newX = startX + xDistanceToMove * percentage; 77 | const newY = startY + yDistanceToMove * percentage; 78 | 79 | self.move(newX, newY); 80 | if (frame.time >= durationInMilliseconds) { 81 | animation.stop(); 82 | self.move(x, y); 83 | } 84 | }, layer); 85 | 86 | animation.start(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries/array.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Libraries.Array", () => { 5 | it("can check if an array contains index", () => { 6 | verifyRuntimeResult(` 7 | x[0] = 1 8 | x["test"] = 1 9 | TextWindow.WriteLine(Array.ContainsIndex(x, 0)) 10 | TextWindow.WriteLine(Array.ContainsIndex(x, "test")) 11 | TextWindow.WriteLine(Array.ContainsIndex(x, 2)) 12 | TextWindow.WriteLine(Array.ContainsIndex(y, 0))`, 13 | [], 14 | [ 15 | "True", 16 | "True", 17 | "False", 18 | "False" 19 | ]); 20 | }); 21 | 22 | it("can check if an array contains value", () => { 23 | verifyRuntimeResult(` 24 | x[0] = 1 25 | x[1] = "test" 26 | TextWindow.WriteLine(Array.ContainsValue(x, 0)) 27 | TextWindow.WriteLine(Array.ContainsValue(x, 1)) 28 | TextWindow.WriteLine(Array.ContainsValue(x, "test")) 29 | TextWindow.WriteLine(Array.ContainsValue(y, 0))`, 30 | [], 31 | [ 32 | "False", 33 | "True", 34 | "True", 35 | "False" 36 | ]); 37 | }); 38 | 39 | it("can can get indices of an array", () => { 40 | verifyRuntimeResult(` 41 | ar["first"] = 5 42 | ar[10] = 6 43 | TextWindow.WriteLine(Array.GetAllIndices(0)) ' Nothing 44 | TextWindow.WriteLine(Array.GetAllIndices("a")) ' Nothing 45 | TextWindow.WriteLine(Array.GetAllIndices(x)) ' Nothing 46 | TextWindow.WriteLine(Array.GetAllIndices(ar))`, 47 | [], 48 | [ 49 | "[]", 50 | "[]", 51 | "[]", 52 | `[1="10", 2="first"]` 53 | ]); 54 | }); 55 | 56 | it("can can get item count of an array", () => { 57 | verifyRuntimeResult(` 58 | ar1[0] = 0 59 | ar2[0] = 1 60 | ar2[1] = 2 61 | TextWindow.WriteLine(Array.GetItemCount(0)) ' Nothing 62 | TextWindow.WriteLine(Array.GetItemCount("a")) ' Nothing 63 | TextWindow.WriteLine(Array.GetItemCount(x)) ' Nothing 64 | TextWindow.WriteLine(Array.GetItemCount(ar1)) 65 | TextWindow.WriteLine(Array.GetItemCount(ar2))`, 66 | [], 67 | [ 68 | "0", 69 | "0", 70 | "0", 71 | "1", 72 | "2" 73 | ]); 74 | }); 75 | 76 | it("can can check whether a value is an array or not", () => { 77 | verifyRuntimeResult(` 78 | ar1[0] = 0 79 | TextWindow.WriteLine(Array.IsArray(0)) ' No 80 | TextWindow.WriteLine(Array.IsArray("a")) ' No 81 | TextWindow.WriteLine(Array.IsArray(x)) ' No 82 | TextWindow.WriteLine(Array.IsArray(ar1))`, 83 | [], 84 | [ 85 | "False", 86 | "False", 87 | "False", 88 | "True" 89 | ]); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/compiler/services/hover-service.ts: -------------------------------------------------------------------------------- 1 | import { Compilation } from "../../compiler/compilation"; 2 | import { CompilerRange, CompilerPosition } from "../syntax/ranges"; 3 | import { RuntimeLibraries } from "../runtime/libraries"; 4 | import { SyntaxKind, ObjectAccessExpressionSyntax, IdentifierExpressionSyntax, SyntaxNodeVisitor } from "../syntax/syntax-nodes"; 5 | 6 | export module HoverService { 7 | export interface Result { 8 | text: string[]; 9 | range: CompilerRange; 10 | } 11 | 12 | export function provideHover(compilation: Compilation, position: CompilerPosition): Result | undefined { 13 | for (let i = 0; i < compilation.diagnostics.length; i++) { 14 | const diagnostic = compilation.diagnostics[i]; 15 | 16 | if (diagnostic.range.containsPosition(position)) { 17 | return { 18 | range: diagnostic.range, 19 | text: [diagnostic.toString()] 20 | }; 21 | } 22 | } 23 | 24 | const node = compilation.getSyntaxNode(position, SyntaxKind.ObjectAccessExpression); 25 | if (node) { 26 | const visitor = new HoverVisitor(); 27 | visitor.visit(node); 28 | return visitor.result; 29 | } 30 | 31 | return undefined; 32 | } 33 | 34 | class HoverVisitor extends SyntaxNodeVisitor { 35 | private _firstResult: Result | undefined; 36 | 37 | public get result(): Result | undefined { 38 | return this._firstResult; 39 | } 40 | 41 | private setResult(result: Result): void { 42 | if (!this._firstResult) { 43 | this._firstResult = result; 44 | } 45 | } 46 | 47 | public visitObjectAccessExpression(node: ObjectAccessExpressionSyntax): void { 48 | if (node.baseExpression.kind !== SyntaxKind.IdentifierExpression) { 49 | return; 50 | } 51 | 52 | const libraryName = (node.baseExpression as IdentifierExpressionSyntax).identifierToken.token.text; 53 | const library = RuntimeLibraries.Metadata[libraryName]; 54 | if (!library) { 55 | return; 56 | } 57 | 58 | let description: string; 59 | const memberName = node.identifierToken.token.text; 60 | if (library.methods[memberName]) { 61 | description = library.methods[memberName].description; 62 | } else if (library.properties[memberName]) { 63 | description = library.properties[memberName].description; 64 | } else { 65 | return; 66 | } 67 | 68 | this.setResult({ 69 | range: node.range, 70 | text: [ 71 | `${libraryName}.${memberName}`, 72 | description 73 | ] 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/compiler/syntax/scanner.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyCompilationErrors } from "../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Syntax.Scanner", () => { 7 | it("reports unterminated string - single", () => { 8 | verifyCompilationErrors(` 9 | x = "name1`, 10 | // x = "name1 11 | // ^^^^^^ 12 | // This string is missing its right double quotes. 13 | new Diagnostic(ErrorCode.UnterminatedStringLiteral, CompilerRange.fromValues(1, 4, 1, 10))); 14 | }); 15 | 16 | it("reports unterminated string - multiple", () => { 17 | verifyCompilationErrors(` 18 | x = "name1 19 | ' Comment line 20 | y = "name2`, 21 | // x = "name1 22 | // ^^^^^^ 23 | // This string is missing its right double quotes. 24 | new Diagnostic(ErrorCode.UnterminatedStringLiteral, CompilerRange.fromValues(1, 4, 1, 10)), 25 | // y = "name2 26 | // ^^^^^^ 27 | // This string is missing its right double quotes. 28 | new Diagnostic(ErrorCode.UnterminatedStringLiteral, CompilerRange.fromValues(3, 4, 3, 10))); 29 | }); 30 | 31 | it("reports unsupported characters - single", () => { 32 | verifyCompilationErrors(` 33 | $`, 34 | // $ 35 | // ^ 36 | // I don't understand this character '$'. 37 | new Diagnostic(ErrorCode.UnrecognizedCharacter, CompilerRange.fromValues(1, 0, 1, 1), "$")); 38 | }); 39 | 40 | it("reports unsupported characters - multiple", () => { 41 | verifyCompilationErrors(` 42 | x = ____^ 43 | ok = "value $ value" 44 | not_ok = "value" $ 45 | & ' Alone`, 46 | // x = ____^ 47 | // ^ 48 | // I don't understand this character '^'. 49 | new Diagnostic(ErrorCode.UnrecognizedCharacter, CompilerRange.fromValues(1, 8, 1, 9), "^"), 50 | // not_ok = "value" $ 51 | // ^ 52 | // I don't understand this character '$'. 53 | new Diagnostic(ErrorCode.UnrecognizedCharacter, CompilerRange.fromValues(3, 17, 3, 18), "$"), 54 | // & ' Alone 55 | // ^ 56 | // I don't understand this character '&'. 57 | new Diagnostic(ErrorCode.UnrecognizedCharacter, CompilerRange.fromValues(4, 0, 4, 1), "&")); 58 | }); 59 | 60 | it("prints an error when parsing a non-supported character in a string (escape character)", () => { 61 | verifyCompilationErrors(` 62 | TextWindow.WriteLine("${String.fromCharCode(27)}")`, 63 | // TextWindow.WriteLine(" ") 64 | // ^ 65 | // I don't understand this character '\u001b'. 66 | new Diagnostic(ErrorCode.UnrecognizedCharacter, CompilerRange.fromValues(1, 22, 1, 22), "\u001b")); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/compiler/runtime/values/array-value.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEngine } from "../../execution-engine"; 2 | import { AddInstruction, DivideInstruction, MultiplyInstruction, SubtractInstruction } from "../../emitting/instructions"; 3 | import { Diagnostic, ErrorCode } from "../../utils/diagnostics"; 4 | import { BaseValue, ValueKind } from "./base-value"; 5 | import { TokenKind } from "../../syntax/tokens"; 6 | import { CompilerUtils } from "../../utils/compiler-utils"; 7 | 8 | export class ArrayValue extends BaseValue { 9 | private _values: { [key: string]: BaseValue }; 10 | 11 | public constructor(value: { readonly [key: string]: BaseValue } = {}) { 12 | super(); 13 | this._values = value; 14 | } 15 | 16 | public get values(): { readonly [key: string]: BaseValue } { 17 | return this._values; 18 | } 19 | 20 | public setIndex(index: string, value: BaseValue): void { 21 | this._values[index] = value; 22 | } 23 | 24 | public deleteIndex(index: string): void { 25 | delete this._values[index]; 26 | } 27 | 28 | public toBoolean(): boolean { 29 | return false; 30 | } 31 | 32 | public toDebuggerString(): string { 33 | return `[${Object.keys(this._values).map(key => `${key}=${this._values[key].toDebuggerString()}`).join(", ")}]`; 34 | } 35 | 36 | public toValueString(): string { 37 | return this.toDebuggerString(); 38 | } 39 | 40 | public get kind(): ValueKind { 41 | return ValueKind.Array; 42 | } 43 | 44 | public tryConvertToNumber(): BaseValue { 45 | return this; 46 | } 47 | 48 | public isEqualTo(other: BaseValue): boolean { 49 | switch (other.kind) { 50 | case ValueKind.String: 51 | case ValueKind.Number: 52 | return false; 53 | case ValueKind.Array: 54 | return this.toDebuggerString() === other.toDebuggerString(); 55 | default: 56 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 57 | } 58 | } 59 | 60 | public isLessThan(_: BaseValue): boolean { 61 | return false; 62 | } 63 | 64 | public isGreaterThan(_: BaseValue): boolean { 65 | return false; 66 | } 67 | 68 | public add(_: BaseValue, engine: ExecutionEngine, instruction: AddInstruction): BaseValue { 69 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Plus))); 70 | return this; 71 | } 72 | 73 | public subtract(_: BaseValue, engine: ExecutionEngine, instruction: SubtractInstruction): BaseValue { 74 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Minus))); 75 | return this; 76 | } 77 | 78 | public multiply(_: BaseValue, engine: ExecutionEngine, instruction: MultiplyInstruction): BaseValue { 79 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Multiply))); 80 | return this; 81 | } 82 | 83 | public divide(_: BaseValue, engine: ExecutionEngine, instruction: DivideInstruction): BaseValue { 84 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Divide))); 85 | return this; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/array.ts: -------------------------------------------------------------------------------- 1 | import { ValueKind, Constants, BaseValue } from "../values/base-value"; 2 | import { StringValue } from "../values/string-value"; 3 | import { ArrayValue } from "../values/array-value"; 4 | import { NumberValue } from "../values/number-value"; 5 | import { ExecutionEngine } from "../../execution-engine"; 6 | import { LibraryMethodInstance, LibraryTypeInstance, LibraryPropertyInstance, LibraryEventInstance } from "../libraries"; 7 | 8 | export class ArrayLibrary implements LibraryTypeInstance { 9 | private executeIsArray(engine: ExecutionEngine): void { 10 | const value = engine.popEvaluationStack(); 11 | engine.pushEvaluationStack(new StringValue(value.kind === ValueKind.Array ? Constants.True : Constants.False)); 12 | } 13 | 14 | private executeGetItemCount(engine: ExecutionEngine): void { 15 | const array = engine.popEvaluationStack(); 16 | const itemCount = array.kind === ValueKind.Array 17 | ? Object.keys((array as ArrayValue).values).length 18 | : 0; 19 | 20 | engine.pushEvaluationStack(new NumberValue(itemCount)); 21 | } 22 | 23 | private executeGetAllIndices(engine: ExecutionEngine): void { 24 | const array = engine.popEvaluationStack(); 25 | const newArray: { [key: string]: BaseValue } = {}; 26 | 27 | if (array.kind === ValueKind.Array) { 28 | Object.keys((array as ArrayValue).values).forEach((key, i) => { 29 | newArray[i + 1] = new StringValue(key); 30 | }); 31 | } 32 | 33 | engine.pushEvaluationStack(new ArrayValue(newArray)); 34 | } 35 | 36 | private executeContainsValue(engine: ExecutionEngine): void { 37 | const value = engine.popEvaluationStack(); 38 | const array = engine.popEvaluationStack(); 39 | let result = Constants.False; 40 | 41 | if (array.kind === ValueKind.Array) { 42 | const arrayValue = (array as ArrayValue).values; 43 | for (let key in arrayValue) { 44 | if (arrayValue[key].isEqualTo(value)) { 45 | result = Constants.True; 46 | break; 47 | } 48 | } 49 | } 50 | 51 | engine.pushEvaluationStack(new StringValue(result)); 52 | } 53 | 54 | private executeContainsIndex(engine: ExecutionEngine): void { 55 | const index = engine.popEvaluationStack().tryConvertToNumber(); 56 | const array = engine.popEvaluationStack(); 57 | let result = Constants.False; 58 | 59 | if (array.kind === ValueKind.Array) { 60 | if (index.kind === ValueKind.Number || index.kind === ValueKind.String) 61 | if ((array as ArrayValue).values[index.toValueString()]) { 62 | result = Constants.True; 63 | } 64 | } 65 | 66 | engine.pushEvaluationStack(new StringValue(result)); 67 | } 68 | 69 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = { 70 | IsArray: { execute: this.executeIsArray.bind(this) }, 71 | GetItemCount: { execute: this.executeGetItemCount.bind(this) }, 72 | GetAllIndices: { execute: this.executeGetAllIndices.bind(this) }, 73 | ContainsValue: { execute: this.executeContainsValue.bind(this) }, 74 | ContainsIndex: { execute: this.executeContainsIndex.bind(this) } 75 | }; 76 | 77 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = {}; 78 | 79 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 80 | } 81 | -------------------------------------------------------------------------------- /src/strings/diagnostics.ts: -------------------------------------------------------------------------------- 1 | // This file is generated through a build task. Do not edit by hand. 2 | 3 | export module DiagnosticsResources { 4 | export const UnrecognizedCharacter = "I don't understand this character '{0}'."; 5 | export const UnterminatedStringLiteral = "This string is missing its right double quotes."; 6 | export const UnrecognizedCommand = "'{0}' is not a valid command."; 7 | export const UnexpectedToken_ExpectingExpression = "Unexpected '{0}' here. I was expecting an expression instead."; 8 | export const UnexpectedToken_ExpectingToken = "Unexpected '{0}' here. I was expecting a token of type '{1}' instead."; 9 | export const UnexpectedToken_ExpectingEOL = "Unexpected '{0}' here. I was expecting a new line after the previous command."; 10 | export const UnexpectedEOL_ExpectingExpression = "Unexpected end of line here. I was expecting an expression instead."; 11 | export const UnexpectedEOL_ExpectingToken = "Unexpected end of line here. I was expecting a token of type '{0}' instead."; 12 | export const UnexpectedCommand_ExpectingCommand = "Unexpected command of type '{0}'. I was expecting a command of type '{1}'."; 13 | export const UnexpectedEOF_ExpectingCommand = "Unexpected end of file. I was expecting a command of type '{0}'."; 14 | export const CannotDefineASubInsideAnotherSub = "You cannot define a sub-module inside another sub-module."; 15 | export const CannotHaveCommandWithoutPreviousCommand = "You cannot write a command of type '{0}' without an earlier command of type '{1}'."; 16 | export const TwoSubModulesWithTheSameName = "Another sub-module with the same name '{0}' is already defined."; 17 | export const LabelDoesNotExist = "No label with the name '{0}' exists in the same module."; 18 | export const UnassignedExpressionStatement = "This value is not assigned to anything. Did you mean to assign it to a variable?"; 19 | export const InvalidExpressionStatement = "This expression is not a valid statement."; 20 | export const UnexpectedVoid_ExpectingValue = "This expression must return a value to be used here."; 21 | export const UnsupportedArrayBaseExpression = "This expression is not a valid array."; 22 | export const UnsupportedCallBaseExpression = "This expression is not a valid submodule or method to be called."; 23 | export const UnexpectedArgumentsCount = "I was expecting {0} arguments, but found {1} instead."; 24 | export const PropertyHasNoSetter = "This property cannot be set. You can only get its value."; 25 | export const UnsupportedDotBaseExpression = "You can only use dot access with a library. Did you mean to use an existing library instead?"; 26 | export const LibraryMemberNotFound = "The library '{0}' has no member named '{1}'."; 27 | export const ValueIsNotANumber = "The value '{0}' is not a valid number."; 28 | export const ValueIsNotAssignable = "You cannot assign to this expression. Did you mean to use a variable instead?"; 29 | export const CannotUseAnArrayAsAnIndexToAnotherArray = "You cannot use an array as an index to access another array. Did you mean to use a string or a number instead?"; 30 | export const CannotUseOperatorWithAnArray = "You cannot use the operator '{0}' with an array value"; 31 | export const CannotUseOperatorWithAString = "You cannot use the operator '{0}' with a string value"; 32 | export const CannotDivideByZero = "You cannot divide by zero. Please consider checking the divisor before dividing."; 33 | export const PoppingAnEmptyStack = "This stack has no elements to be popped"; 34 | 35 | export function get(key: string): string { 36 | return (DiagnosticsResources)[key]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/common/custom-editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { detect } from "detect-browser"; 2 | import * as React from "react"; 3 | import { Diagnostic } from "../../../../compiler/utils/diagnostics"; 4 | import { EditorUtils } from "../../../editor-utils"; 5 | import { CompilerRange } from "../../../../compiler/syntax/ranges"; 6 | 7 | import "./styles/style.css"; 8 | import "./styles/monaco-override.css"; 9 | import { EditorCompletionService } from "./services/completion-service"; 10 | import { EditorHoverService } from "./services/hover-service"; 11 | 12 | interface CustomEditorProps { 13 | readOnly: boolean; 14 | initialValue: string; 15 | } 16 | 17 | // TODO: review and fix for all browsers 18 | const browser = detect(); 19 | let displayNewStyle = false; 20 | switch (browser && browser.name) { 21 | case "chrome": 22 | case "firefox": 23 | displayNewStyle = true; 24 | break; 25 | } 26 | 27 | monaco.languages.registerCompletionItemProvider("sb", new EditorCompletionService()); 28 | monaco.languages.registerHoverProvider("sb", new EditorHoverService()); 29 | 30 | export class CustomEditor extends React.Component { 31 | public editor?: monaco.editor.IStandaloneCodeEditor; 32 | private editorDiv?: HTMLDivElement; 33 | private onResize?: () => void; 34 | 35 | private decorations: string[] = []; 36 | 37 | public render(): JSX.Element { 38 | return
this.editorDiv = div!} 41 | style={{ height: "100%", width: "100%" }} />; 42 | } 43 | 44 | public componentDidMount(): void { 45 | const options: monaco.editor.IEditorConstructionOptions = { 46 | language: "sb", 47 | scrollBeyondLastLine: false, 48 | readOnly: this.props.readOnly, 49 | value: this.props.initialValue, 50 | fontFamily: "Consolas, monospace, Hack", 51 | fontSize: 18, 52 | glyphMargin: true, 53 | minimap: { 54 | enabled: false 55 | } 56 | }; 57 | 58 | this.editor = (window as any).monaco.editor.create(this.editorDiv, options); 59 | 60 | this.onResize = () => { 61 | this.editor!.layout(); 62 | }; 63 | 64 | window.addEventListener("resize", this.onResize); 65 | } 66 | 67 | public componentWillUnmount(): void { 68 | this.editor!.dispose(); 69 | window.removeEventListener("resize", this.onResize!); 70 | } 71 | 72 | public undo(): void { 73 | this.editor!.trigger("", "undo", ""); 74 | } 75 | 76 | public redo(): void { 77 | this.editor!.trigger("", "redo", ""); 78 | } 79 | 80 | public setDiagnostics(diagnostics: ReadonlyArray): void { 81 | this.decorations = this.editor!.deltaDecorations(this.decorations, diagnostics.map(diagnostic => { 82 | return { 83 | range: EditorUtils.compilerRangeToEditorRange(diagnostic.range), 84 | options: { 85 | className: "wavy-line", 86 | glyphMarginClassName: "error-line-glyph" 87 | } 88 | }; 89 | })); 90 | } 91 | 92 | public highlightLine(line: number): void { 93 | const range = EditorUtils.compilerRangeToEditorRange(CompilerRange.fromValues(line, 0, line, Number.MAX_VALUE)); 94 | 95 | this.decorations = this.editor!.deltaDecorations(this.decorations, [{ 96 | range: range, 97 | options: { 98 | className: "debugger-line-highlight" 99 | } 100 | }]); 101 | 102 | this.editor!.revealLine(range.startLineNumber); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/compiler/services/completion-service.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeLibraries } from "../runtime/libraries"; 2 | import { CompilerPosition } from "../syntax/ranges"; 3 | import { Compilation } from "../compilation"; 4 | import { CompilerUtils } from "../utils/compiler-utils"; 5 | import { SyntaxNodeVisitor, ObjectAccessExpressionSyntax, SyntaxKind, IdentifierExpressionSyntax } from "../syntax/syntax-nodes"; 6 | import { CommandsParser } from "../syntax/command-parser"; 7 | 8 | export module CompletionService { 9 | export enum ResultKind { 10 | Class, 11 | Method, 12 | Property 13 | } 14 | 15 | export interface Result { 16 | kind: ResultKind; 17 | title: string; 18 | description: string; 19 | } 20 | 21 | export function provideCompletion(compilation: Compilation, position: CompilerPosition): Result[] { 22 | const objectAccessExpression = compilation.getSyntaxNode(position, SyntaxKind.ObjectAccessExpression); 23 | if (objectAccessExpression) { 24 | const visitor = new CompletionVisitor(); 25 | visitor.visit(objectAccessExpression); 26 | return visitor.results; 27 | } 28 | 29 | const identifierExpression = compilation.getSyntaxNode(position, SyntaxKind.IdentifierExpression); 30 | if (identifierExpression) { 31 | const visitor = new CompletionVisitor(); 32 | visitor.visit(identifierExpression); 33 | return visitor.results; 34 | } 35 | 36 | return []; 37 | } 38 | 39 | class CompletionVisitor extends SyntaxNodeVisitor { 40 | private _allResults: Result[] = []; 41 | 42 | public get results(): Result[] { 43 | return this._allResults; 44 | } 45 | 46 | private addResult(result: Result): void { 47 | this._allResults.push(result); 48 | } 49 | 50 | public visitObjectAccessExpression(node: ObjectAccessExpressionSyntax): void { 51 | if (node.baseExpression.kind !== SyntaxKind.IdentifierExpression) { 52 | return; 53 | } 54 | 55 | const libraryName = (node.baseExpression as IdentifierExpressionSyntax).identifierToken.token.text; 56 | const library = RuntimeLibraries.Metadata[libraryName]; 57 | if (!library) { 58 | return; 59 | } 60 | 61 | let memberName = node.identifierToken.token.text; 62 | if (memberName === CommandsParser.MissingTokenText) { 63 | memberName = ""; 64 | } 65 | 66 | CompilerUtils.values(library.methods).forEach(method => { 67 | if (CompilerUtils.stringStartsWith(method.methodName, memberName)) { 68 | this.addResult({ 69 | title: method.methodName, 70 | description: method.description, 71 | kind: ResultKind.Method 72 | }); 73 | } 74 | }); 75 | 76 | CompilerUtils.values(library.properties).forEach(property => { 77 | if (CompilerUtils.stringStartsWith(property.propertyName, memberName)) { 78 | this.addResult({ 79 | title: property.propertyName, 80 | description: property.description, 81 | kind: ResultKind.Property 82 | }); 83 | } 84 | }); 85 | } 86 | 87 | public visitIdentifierExpression(node: IdentifierExpressionSyntax): void { 88 | const libraryName = node.identifierToken.token.text; 89 | 90 | CompilerUtils.values(RuntimeLibraries.Metadata).forEach(library => { 91 | if (CompilerUtils.stringStartsWith(library.typeName, libraryName)) { 92 | this.addResult({ 93 | title: library.typeName, 94 | description: library.description, 95 | kind: ResultKind.Class 96 | }); 97 | } 98 | }); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/math.ts: -------------------------------------------------------------------------------- 1 | import { LibraryTypeInstance, LibraryMethodInstance, LibraryPropertyInstance, LibraryEventInstance } from "../libraries"; 2 | import { BaseValue, ValueKind } from "../values/base-value"; 3 | import { NumberValue } from "../values/number-value"; 4 | import { ExecutionEngine, ExecutionMode, ExecutionState } from "../../execution-engine"; 5 | import { CompilerRange } from "../../syntax/ranges"; 6 | import { Diagnostic, ErrorCode } from "../../utils/diagnostics"; 7 | 8 | export class MathLibrary implements LibraryTypeInstance { 9 | private getPi(): BaseValue { 10 | return new NumberValue(Math.PI); 11 | } 12 | 13 | private executeCalculation(engine: ExecutionEngine, calculation: (...values: number[]) => number): void { 14 | const args: number[] = new Array(calculation.length); 15 | for (let i = args.length - 1; i >= 0; i--) { 16 | const value = engine.popEvaluationStack().tryConvertToNumber(); 17 | 18 | if (value.kind === ValueKind.Number) { 19 | args[i] = (value as NumberValue).value; 20 | } else { 21 | engine.pushEvaluationStack(new NumberValue(0)); 22 | return; 23 | } 24 | } 25 | 26 | const result = calculation(...args); 27 | if (engine.state !== ExecutionState.Terminated) { 28 | engine.pushEvaluationStack(new NumberValue(result)); 29 | } 30 | } 31 | 32 | private executeRemainder(engine: ExecutionEngine, _: ExecutionMode, range: CompilerRange): void { 33 | return this.executeCalculation(engine, (dividend, divisor) => { 34 | if (divisor === 0) { 35 | engine.terminate(new Diagnostic(ErrorCode.CannotDivideByZero, range)); 36 | return 0; 37 | } 38 | 39 | return dividend % divisor; 40 | }); 41 | } 42 | 43 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = { 44 | Abs: { execute: engine => this.executeCalculation(engine, Math.abs) }, 45 | Remainder: { execute: this.executeRemainder.bind(this) }, 46 | 47 | Cos: { execute: engine => this.executeCalculation(engine, Math.cos) }, 48 | Sin: { execute: engine => this.executeCalculation(engine, Math.sin) }, 49 | Tan: { execute: engine => this.executeCalculation(engine, Math.tan) }, 50 | 51 | ArcCos: { execute: engine => this.executeCalculation(engine, Math.acos) }, 52 | ArcSin: { execute: engine => this.executeCalculation(engine, Math.asin) }, 53 | ArcTan: { execute: engine => this.executeCalculation(engine, Math.atan) }, 54 | 55 | Ceiling: { execute: engine => this.executeCalculation(engine, Math.ceil) }, 56 | Floor: { execute: engine => this.executeCalculation(engine, Math.floor) }, 57 | Round: { execute: engine => this.executeCalculation(engine, Math.round) }, 58 | 59 | GetDegrees: { execute: engine => this.executeCalculation(engine, angle => 180 * angle / Math.PI % 360) }, 60 | GetRadians: { execute: engine => this.executeCalculation(engine, angle => angle % 360 * Math.PI / 180) }, 61 | 62 | GetRandomNumber: { execute: engine => this.executeCalculation(engine, maxNumber => Math.floor(Math.random() * (Math.max(1, maxNumber) - 1)) + 1) }, 63 | 64 | Log: { execute: engine => this.executeCalculation(engine, value => Math.log(value) / Math.LN10) }, 65 | NaturalLog: { execute: engine => this.executeCalculation(engine, Math.log) }, 66 | 67 | Max: { execute: engine => this.executeCalculation(engine, (value1, value2) => Math.max(value1, value2)) }, 68 | Min: { execute: engine => this.executeCalculation(engine, (value1, value2) => Math.min(value1, value2)) }, 69 | 70 | Power: { execute: engine => this.executeCalculation(engine, (baseNumber, exponent) => Math.pow(baseNumber, exponent)) }, 71 | SquareRoot: { execute: engine => this.executeCalculation(engine, value => value < 0 ? 0 : Math.sqrt(value)) } 72 | }; 73 | 74 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = { 75 | Pi: { getter: this.getPi.bind(this) } 76 | }; 77 | 78 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 79 | } 80 | -------------------------------------------------------------------------------- /src/app/components/run/index.tsx: -------------------------------------------------------------------------------- 1 | import { MasterLayoutComponent } from "../common/master-layout"; 2 | import { ToolbarButton } from "../common/toolbar-button"; 3 | import * as React from "react"; 4 | import { EditorResources } from "../../../strings/editor"; 5 | import { RouteComponentProps, withRouter } from "react-router"; 6 | import { Compilation } from "../../../compiler/compilation"; 7 | import { AppState } from "../../store"; 8 | import { Dispatch, connect } from "react-redux"; 9 | import { ExecutionMode, ExecutionEngine } from "../../../compiler/execution-engine"; 10 | import { TextWindowComponent } from "../common/text-window/index"; 11 | import { GraphicsWindowComponent } from "../common/graphics-window/index"; 12 | 13 | const StopIcon = require("../../content/buttons/stop.png"); 14 | 15 | interface PropsFromState { 16 | compilation: Compilation; 17 | appInsights: Microsoft.ApplicationInsights.IAppInsights; 18 | } 19 | 20 | interface PropsFromDispatch { 21 | } 22 | 23 | interface PropsFromReact { 24 | } 25 | 26 | type PresentationalComponentProps = PropsFromState & PropsFromDispatch & PropsFromReact & RouteComponentProps; 27 | 28 | interface PresentationalComponentState { 29 | engine: ExecutionEngine; 30 | } 31 | 32 | class PresentationalComponent extends React.Component { 33 | private isAlreadyMounted: boolean = false; 34 | 35 | public constructor(props: PresentationalComponentProps) { 36 | super(props); 37 | 38 | if (!this.props.compilation.isReadyToRun) { 39 | this.props.history.push("/editor"); 40 | } 41 | 42 | this.state = { 43 | engine: new ExecutionEngine(this.props.compilation) 44 | }; 45 | } 46 | 47 | public componentDidMount(): void { 48 | this.isAlreadyMounted = true; 49 | this.props.appInsights.trackPageView("RunPage"); 50 | setTimeout(this.execute.bind(this)); 51 | } 52 | 53 | public componentWillUnmount(): void { 54 | this.isAlreadyMounted = false; 55 | } 56 | 57 | private execute(): void { 58 | if (this.isAlreadyMounted) { 59 | this.state.engine.execute(ExecutionMode.RunToEnd); 60 | setTimeout(this.execute.bind(this)); 61 | } 62 | } 63 | 64 | public render(): JSX.Element { 65 | return ( 66 | this.props.history.push("/editor")} /> 73 | ]} 74 | masterContainer={ 75 | this.props.compilation.kind.drawsShapes 76 | ? 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | : 86 |
87 | 88 |
89 | } 90 | /> 91 | ); 92 | } 93 | } 94 | 95 | function mapStateToProps(state: AppState): PropsFromState { 96 | return { 97 | compilation: state.compilation, 98 | appInsights: state.appInsights 99 | }; 100 | } 101 | 102 | function mapDispatchToProps(_: Dispatch): PropsFromDispatch { 103 | return { 104 | }; 105 | } 106 | 107 | export const RunComponent = connect(mapStateToProps, mapDispatchToProps)(withRouter(PresentationalComponent as any)); 108 | -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as gulp from "gulp"; 3 | import * as path from "path"; 4 | import * as helpers from "./build/gulp-helpers"; 5 | import { generateLocStrings } from "./build/generate-loc-strings"; 6 | 7 | const userLanguage = "en"; 8 | 9 | gulp.task("generate-diagnostics-strings", () => generateLocStrings("DiagnosticsResources", path.resolve(__dirname, `./build/strings/${userLanguage}/diagnostics.json`), path.resolve(__dirname, `./src/strings/diagnostics.ts`))); 10 | gulp.task("generate-compiler-strings", () => generateLocStrings("CompilerResources", path.resolve(__dirname, `./build/strings/${userLanguage}/compiler.json`), path.resolve(__dirname, `./src/strings/compiler.ts`))); 11 | gulp.task("generate-documentation-strings", () => generateLocStrings("DocumentationResources", path.resolve(__dirname, `./build/strings/${userLanguage}/documentation.json`), path.resolve(__dirname, `./src/strings/documentation.ts`))); 12 | gulp.task("generate-app-editor-strings", () => generateLocStrings("EditorResources", path.resolve(__dirname, `./build/strings/${userLanguage}/editor.json`), path.resolve(__dirname, `./src/strings/editor.ts`))); 13 | 14 | gulp.task("generate-loc-strings", [ 15 | "generate-diagnostics-strings", 16 | "generate-compiler-strings", 17 | "generate-documentation-strings", 18 | "generate-app-editor-strings", 19 | ]); 20 | 21 | gulp.task("build-source-debug", ["generate-loc-strings"], () => helpers.runWebpack({ projectPath: "./src/app/webpack.config.ts", release: false, watch: false })); 22 | gulp.task("build-source-release", ["generate-loc-strings"], () => helpers.runWebpack({ projectPath: "./src/app/webpack.config.ts", release: true, watch: false })); 23 | 24 | gulp.task("watch-source", () => { 25 | gulp.watch("build/**", ["generate-loc-strings"]); 26 | return helpers.runWebpack({ 27 | projectPath: "./src/app/webpack.config.ts", 28 | release: false, 29 | watch: true 30 | }); 31 | }); 32 | 33 | gulp.task("build-tests-debug", () => helpers.runWebpack({ projectPath: "./tests/webpack.config.ts", release: false, watch: false })); 34 | gulp.task("run-tests-debug", ["build-tests-debug"], () => helpers.cmdToPromise("node", ["./node_modules/jasmine/bin/jasmine.js", "./out/tests/tests.js"])); 35 | 36 | gulp.task("build-tests-release", () => helpers.runWebpack({ projectPath: "./tests/webpack.config.ts", release: true, watch: false })); 37 | gulp.task("run-tests-release", ["build-tests-release"], () => helpers.cmdToPromise("node", ["./node_modules/jasmine/bin/jasmine.js", "./out/tests/tests.js"])); 38 | 39 | gulp.task("watch-tests", () => { 40 | gulp.watch("build/**", ["generate-loc-strings"]); 41 | gulp.watch(["src/**", "tests/**"], ["run-tests-debug"]); 42 | }); 43 | 44 | function packageFunction(release: boolean): Promise { 45 | const setupConfigPath = "./out/electron/electron-builder-config.json"; 46 | const electronBuilderPath = path.resolve(__dirname, "./node_modules/.bin/electron-builder.cmd"); 47 | 48 | return helpers.rimrafToPromise("./out/electron") 49 | .then(() => helpers.runWebpack({ 50 | projectPath: "./src/electron/webpack.config.ts", 51 | release: release, 52 | watch: false 53 | })) 54 | .then(() => helpers.streamToPromise(gulp.src("./out/app/**").pipe(gulp.dest("./out/electron")))) 55 | .then(() => helpers.streamToPromise(gulp.src("./package.json").pipe(gulp.dest("./out/electron")))) 56 | .then(() => helpers.rimrafToPromise("./out/installers")) 57 | .then(() => new Promise((resolve, reject) => { 58 | const config = { 59 | productName: "SmallBasic-Online", 60 | directories: { 61 | app: "./out/electron", 62 | output: "./out/installers" 63 | }, 64 | win: { 65 | target: [ 66 | { target: "nsis", arch: ["ia32"] } 67 | ], 68 | icon: "./src/electron/installer" 69 | } 70 | }; 71 | fs.writeFile(setupConfigPath, JSON.stringify(config), "utf8", error => { 72 | if (error) { 73 | reject(error); 74 | } else { 75 | resolve(); 76 | } 77 | }); 78 | })) 79 | .then(() => helpers.cmdToPromise(electronBuilderPath, ["build", "--config", setupConfigPath, "--publish", "never"])); 80 | } 81 | 82 | gulp.task("package-debug", ["build-source-debug"], () => packageFunction(false)); 83 | gulp.task("package-release", ["build-source-release"], () => packageFunction(true)); 84 | -------------------------------------------------------------------------------- /src/compiler/compilation.ts: -------------------------------------------------------------------------------- 1 | import { ModuleEmitter } from "./emitting/module-emitter"; 2 | import { BaseInstruction } from "./emitting/instructions"; 3 | import { CommandsParser } from "./syntax/command-parser"; 4 | import { Diagnostic } from "./utils/diagnostics"; 5 | import { ModulesBinder } from "./binding/modules-binder"; 6 | import { Scanner } from "./syntax/scanner"; 7 | import { Token } from "./syntax/tokens"; 8 | import { StatementsParser } from "./syntax/statements-parser"; 9 | import { BaseSyntaxNode, ParseTreeSyntax, SyntaxKind, SyntaxNodeVisitor, IdentifierExpressionSyntax } from "./syntax/syntax-nodes"; 10 | import { BoundStatementBlock } from "./binding/bound-nodes"; 11 | import { CompilerPosition } from "./syntax/ranges"; 12 | 13 | export interface CompilationKind { 14 | writesToTextWindow(): boolean; 15 | drawsShapes(): boolean; 16 | } 17 | 18 | export class Compilation { 19 | private _outputKindDetector?: OutputKindDetector; 20 | 21 | public readonly tokens: ReadonlyArray; 22 | public readonly parseTree: ParseTreeSyntax; 23 | public readonly boundSubModules: { [name: string]: BoundStatementBlock }; 24 | public readonly diagnostics: Diagnostic[] = []; 25 | 26 | public get isReadyToRun(): boolean { 27 | return !!this.text.trim() && !this.diagnostics.length; 28 | } 29 | 30 | public get kind(): CompilationKind { 31 | if (!this._outputKindDetector) { 32 | this._outputKindDetector = new OutputKindDetector(); 33 | this._outputKindDetector.visit(this.parseTree); 34 | } 35 | 36 | return this._outputKindDetector; 37 | } 38 | 39 | public constructor(public readonly text: string) { 40 | this.diagnostics = []; 41 | 42 | this.tokens = new Scanner(this.text, this.diagnostics).result; 43 | 44 | const commands = new CommandsParser(this.tokens, this.diagnostics).result; 45 | this.parseTree = new StatementsParser(commands, this.diagnostics).result; 46 | this.setParentNode(this.parseTree); 47 | 48 | const binder = new ModulesBinder(this.parseTree, this.diagnostics); 49 | this.boundSubModules = binder.boundModules; 50 | } 51 | 52 | public emit(): { readonly [name: string]: ReadonlyArray } { 53 | if (!this.isReadyToRun) { 54 | throw new Error(`Cannot emit an empty or errornous compilation`); 55 | } 56 | 57 | const result: { [name: string]: ReadonlyArray } = {}; 58 | for (const name in this.boundSubModules) { 59 | result[name] = new ModuleEmitter(this.boundSubModules[name]).instructions; 60 | } 61 | 62 | return result; 63 | } 64 | 65 | public getSyntaxNode(position: CompilerPosition, kind: SyntaxKind): BaseSyntaxNode | undefined { 66 | function getSyntaxNodeAux(node: BaseSyntaxNode, position: CompilerPosition): BaseSyntaxNode | undefined { 67 | if (node.range.containsPosition(position)) { 68 | let children = node.children(); 69 | for (let i = 0; i < children.length; i++) { 70 | const result = getSyntaxNodeAux(children[i], position); 71 | if (result) { 72 | return result; 73 | } 74 | } 75 | if (node.kind === kind) { 76 | return node; 77 | } 78 | } 79 | return undefined; 80 | } 81 | 82 | return getSyntaxNodeAux(this.parseTree, position); 83 | } 84 | 85 | private setParentNode(node: BaseSyntaxNode): void { 86 | node.children().forEach(child => { 87 | child.parentOpt = node; 88 | this.setParentNode(child); 89 | }); 90 | } 91 | } 92 | 93 | class OutputKindDetector extends SyntaxNodeVisitor implements CompilationKind { 94 | private _writesToTextWindow: boolean = false; 95 | private _drawsShapes: boolean = false; 96 | 97 | public writesToTextWindow(): boolean { 98 | return this._writesToTextWindow; 99 | } 100 | 101 | public drawsShapes(): boolean { 102 | return this._drawsShapes; 103 | } 104 | 105 | public visitIdentifierExpression(node: IdentifierExpressionSyntax): void { 106 | switch (node.identifierToken.token.text) { 107 | case "TextWindow": 108 | this._writesToTextWindow = true; 109 | break; 110 | case "Shapes": 111 | case "Controls": 112 | case "Turtle": 113 | this._drawsShapes = true; 114 | break; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/compiler/syntax/statements-parser.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyCompilationErrors } from "../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Syntax.StatementsParser", () => { 7 | it("reports errors on unfinished modules", () => { 8 | verifyCompilationErrors(` 9 | Sub A`, 10 | // Sub A 11 | // ^^^^^ 12 | // Unexpected end of file. I was expecting a command of type 'EndSub'. 13 | new Diagnostic(ErrorCode.UnexpectedEOF_ExpectingCommand, CompilerRange.fromValues(1, 0, 1, 5), "EndSub")); 14 | }); 15 | 16 | it("reports errors on defining sub inside another one", () => { 17 | verifyCompilationErrors(` 18 | Sub A 19 | Sub B 20 | EndSub`, 21 | // Sub B 22 | // ^^^^^ 23 | // You cannot define a sub-module inside another sub-module. 24 | new Diagnostic(ErrorCode.CannotDefineASubInsideAnotherSub, CompilerRange.fromValues(2, 0, 2, 5))); 25 | }); 26 | 27 | it("reports errors on ending sub without starting one", () => { 28 | verifyCompilationErrors(` 29 | x = 1 30 | EndSub`, 31 | // EndSub 32 | // ^^^^^^ 33 | // You cannot write a command of type 'EndSub' without an earlier command of type 'Sub'. 34 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 6), "EndSub", "Sub")); 35 | }); 36 | 37 | it("reports errors on ElseIf without If", () => { 38 | verifyCompilationErrors(` 39 | x = 1 40 | ElseIf y Then`, 41 | // ElseIf y Then 42 | // ^^^^^^^^^^^^^ 43 | // You cannot write a command of type 'ElseIf' without an earlier command of type 'If'. 44 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 13), "ElseIf", "If")); 45 | }); 46 | 47 | it("reports errors on Else without If", () => { 48 | verifyCompilationErrors(` 49 | x = 1 50 | Else`, 51 | // Else 52 | // ^^^^ 53 | // You cannot write a command of type 'Else' without an earlier command of type 'If'. 54 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 4), "Else", "If")); 55 | }); 56 | 57 | it("reports errors on EndIf without If", () => { 58 | verifyCompilationErrors(` 59 | x = 1 60 | EndIf`, 61 | // EndIf 62 | // ^^^^^ 63 | // You cannot write a command of type 'EndIf' without an earlier command of type 'If'. 64 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 5), "EndIf", "If")); 65 | }); 66 | 67 | it("reports errors on EndFor without For", () => { 68 | verifyCompilationErrors(` 69 | x = 1 70 | EndFor`, 71 | // EndFor 72 | // ^^^^^^ 73 | // You cannot write a command of type 'EndFor' without an earlier command of type 'For'. 74 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 6), "EndFor", "For")); 75 | }); 76 | 77 | it("reports errors on EndWhile without While", () => { 78 | verifyCompilationErrors(` 79 | x = 1 80 | EndWhile`, 81 | // EndWhile 82 | // ^^^^^^^^ 83 | // You cannot write a command of type 'EndWhile' without an earlier command of type 'While'. 84 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(2, 0, 2, 8), "EndWhile", "While")); 85 | }); 86 | 87 | it("reports errors on ElseIf After Else", () => { 88 | verifyCompilationErrors(` 89 | If x Then 90 | a = 0 91 | Else 92 | a = 1 93 | ElseIf y Then 94 | a = 2 95 | EndIf`, 96 | // ElseIf y Then 97 | // ^^^^^^^^^^^^^ 98 | // Unexpected command of type 'ElseIf'. I was expecting a command of type 'EndIf'. 99 | new Diagnostic(ErrorCode.UnexpectedCommand_ExpectingCommand, CompilerRange.fromValues(5, 0, 5, 13), "ElseIf", "EndIf"), 100 | // ElseIf y Then 101 | // ^^^^^^^^^^^^^ 102 | // You cannot write a command of type 'ElseIf' without an earlier command of type 'If'. 103 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(5, 0, 5, 13), "ElseIf", "If"), 104 | // EndIf 105 | // ^^^^^ 106 | // You cannot write a command of type 'EndIf' without an earlier command of type 'If'. 107 | new Diagnostic(ErrorCode.CannotHaveCommandWithoutPreviousCommand, CompilerRange.fromValues(7, 0, 7, 5), "EndIf", "If")); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/compiler/utils/compiler-utils.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxKind } from "../syntax/syntax-nodes"; 2 | import { TokenKind } from "../syntax/tokens"; 3 | import { CompilerResources } from "../../strings/compiler"; 4 | 5 | export module CompilerUtils { 6 | export function formatString(template: string, args: ReadonlyArray): string { 7 | return template.replace(/{[0-9]+}/g, match => args[parseInt(match.replace(/^{/, "").replace(/}$/, ""))]); 8 | } 9 | 10 | export function stringStartsWith(value: string, prefix?: string): boolean { 11 | if (!prefix || !prefix.length) { 12 | return true; 13 | } 14 | 15 | value = value.toLowerCase(); 16 | prefix = prefix.toLocaleLowerCase(); 17 | 18 | if (value.length <= prefix.length) { 19 | return false; 20 | } else { 21 | return prefix === value.substr(0, prefix.length); 22 | } 23 | } 24 | 25 | export function values(parent: { [key: string]: TMember }): ReadonlyArray { 26 | return Object.keys(parent).map(key => parent[key]); 27 | } 28 | 29 | export function commandToDisplayString(kind: SyntaxKind): string { 30 | switch (kind) { 31 | case SyntaxKind.IfCommand: return tokenToDisplayString(TokenKind.IfKeyword); 32 | case SyntaxKind.ElseCommand: return tokenToDisplayString(TokenKind.ElseKeyword); 33 | case SyntaxKind.ElseIfCommand: return tokenToDisplayString(TokenKind.ElseIfKeyword); 34 | case SyntaxKind.EndIfCommand: return tokenToDisplayString(TokenKind.EndIfKeyword); 35 | case SyntaxKind.ForCommand: return tokenToDisplayString(TokenKind.ForKeyword); 36 | case SyntaxKind.EndForCommand: return tokenToDisplayString(TokenKind.EndForKeyword); 37 | case SyntaxKind.WhileCommand: return tokenToDisplayString(TokenKind.WhileKeyword); 38 | case SyntaxKind.EndWhileCommand: return tokenToDisplayString(TokenKind.EndWhileKeyword); 39 | case SyntaxKind.LabelCommand: return CompilerResources.SyntaxNodes_Label; 40 | case SyntaxKind.GoToCommand: return tokenToDisplayString(TokenKind.GoToKeyword); 41 | case SyntaxKind.SubCommand: return tokenToDisplayString(TokenKind.SubKeyword); 42 | case SyntaxKind.EndSubCommand: return tokenToDisplayString(TokenKind.EndSubKeyword); 43 | case SyntaxKind.ExpressionCommand: return CompilerResources.SyntaxNodes_Expression; 44 | default: throw new Error(`Unexpected syntax kind: ${SyntaxKind[kind]}`); 45 | } 46 | } 47 | 48 | export function tokenToDisplayString(kind: TokenKind): string { 49 | switch (kind) { 50 | case TokenKind.IfKeyword: return "If"; 51 | case TokenKind.ThenKeyword: return "Then"; 52 | case TokenKind.ElseKeyword: return "Else"; 53 | case TokenKind.ElseIfKeyword: return "ElseIf"; 54 | case TokenKind.EndIfKeyword: return "EndIf"; 55 | case TokenKind.ForKeyword: return "For"; 56 | case TokenKind.ToKeyword: return "To"; 57 | case TokenKind.StepKeyword: return "Step"; 58 | case TokenKind.EndForKeyword: return "EndFor"; 59 | case TokenKind.GoToKeyword: return "GoTo"; 60 | case TokenKind.WhileKeyword: return "While"; 61 | case TokenKind.EndWhileKeyword: return "EndWhile"; 62 | case TokenKind.SubKeyword: return "Sub"; 63 | case TokenKind.EndSubKeyword: return "EndSub"; 64 | 65 | case TokenKind.Dot: return "."; 66 | case TokenKind.RightParen: return ")"; 67 | case TokenKind.LeftParen: return "("; 68 | case TokenKind.RightSquareBracket: return "]"; 69 | case TokenKind.LeftSquareBracket: return "["; 70 | case TokenKind.Comma: return ","; 71 | case TokenKind.Equal: return "="; 72 | case TokenKind.NotEqual: return "<>"; 73 | case TokenKind.Plus: return "+"; 74 | case TokenKind.Minus: return "-"; 75 | case TokenKind.Multiply: return "*"; 76 | case TokenKind.Divide: return "/"; 77 | case TokenKind.Colon: return ":"; 78 | case TokenKind.LessThan: return "<"; 79 | case TokenKind.GreaterThan: return ">"; 80 | case TokenKind.LessThanOrEqual: return "<="; 81 | case TokenKind.GreaterThanOrEqual: return ">="; 82 | case TokenKind.Or: return "Or"; 83 | case TokenKind.And: return "And"; 84 | 85 | case TokenKind.Identifier: return CompilerResources.SyntaxNodes_Identifier; 86 | case TokenKind.NumberLiteral: return CompilerResources.SyntaxNodes_NumberLiteral; 87 | case TokenKind.StringLiteral: return CompilerResources.SyntaxNodes_StringLiteral; 88 | case TokenKind.Comment: return CompilerResources.SyntaxNodes_Comment; 89 | 90 | default: throw new Error(`Unrecognized token kind: ${TokenKind[kind]}`); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries/text-window.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult } from "../../helpers"; 3 | 4 | describe("Compiler.Runtime.Libraries.TextWindow", () => { 5 | it("no input or output", () => { 6 | verifyRuntimeResult(` 7 | If "Truee" Then 8 | TextWindow.WriteLine(5) 9 | EndIf`); 10 | }); 11 | 12 | it("reads and writes a string value", () => { 13 | verifyRuntimeResult(` 14 | x = TextWindow.Read() 15 | TextWindow.WriteLine(x) 16 | TextWindow.WriteLine(x + 1)`, 17 | ["test"], 18 | ["test", "test1"]); 19 | }); 20 | 21 | it("reads and writes a number value as string", () => { 22 | verifyRuntimeResult(` 23 | x = TextWindow.ReadNumber() 24 | TextWindow.WriteLine(x) 25 | TextWindow.WriteLine(x + 1)`, 26 | [5], 27 | ["5", "6"]); 28 | }); 29 | 30 | it("displays the contents of an array", () => { 31 | verifyRuntimeResult(` 32 | x[0] = 1 33 | x[1] = "test" 34 | x[2][0] = 10 35 | x[2][1] = 11 36 | x["key"] = "value" 37 | TextWindow.WriteLine(x) 38 | 39 | x["2"] = -5 40 | TextWindow.WriteLine(x) 41 | 42 | TextWindow.WriteLine(x["key"])`, 43 | [], 44 | [ 45 | `[0=1, 1="test", 2=[0=10, 1=11], key="value"]`, 46 | `[0=1, 1="test", 2=-5, key="value"]`, 47 | `value` 48 | ]); 49 | }); 50 | 51 | it("prints non-conventional characters", () => { 52 | verifyRuntimeResult(` 53 | TextWindow.WriteLine("$")`, 54 | [], 55 | ["$"]); 56 | }); 57 | 58 | it("sets foreground color correctly", () => { 59 | verifyRuntimeResult(` 60 | TextWindow.WriteLine(TextWindow.ForegroundColor) 61 | TextWindow.ForegroundColor = 7 62 | TextWindow.WriteLine(TextWindow.ForegroundColor) 63 | TextWindow.ForegroundColor = "Blue" 64 | TextWindow.WriteLine(TextWindow.ForegroundColor) 65 | TextWindow.ForegroundColor = "cyan" ' Case insensitive 66 | TextWindow.WriteLine(TextWindow.ForegroundColor)`, 67 | [], 68 | [ 69 | "White", 70 | "Gray", 71 | "Blue", 72 | "Cyan" 73 | ]); 74 | }); 75 | 76 | it("sets background color correctly", () => { 77 | verifyRuntimeResult(` 78 | TextWindow.WriteLine(TextWindow.BackgroundColor) 79 | TextWindow.BackgroundColor = 7 80 | TextWindow.WriteLine(TextWindow.BackgroundColor) 81 | TextWindow.BackgroundColor = "Blue" 82 | TextWindow.WriteLine(TextWindow.BackgroundColor) 83 | TextWindow.BackgroundColor = "cyan" ' Case insensitive 84 | TextWindow.WriteLine(TextWindow.BackgroundColor)`, 85 | [], 86 | [ 87 | "Black", 88 | "Gray", 89 | "Blue", 90 | "Cyan" 91 | ]); 92 | }); 93 | 94 | it("does not change when an invalid number is used for background color", () => { 95 | verifyRuntimeResult(` 96 | TextWindow.BackgroundColor = "Red" 97 | TextWindow.WriteLine(TextWindow.BackgroundColor) 98 | TextWindow.BackgroundColor = 9455 99 | TextWindow.WriteLine(TextWindow.BackgroundColor) 100 | `, 101 | [], 102 | [ 103 | "Red", 104 | "Red" 105 | ]); 106 | }); 107 | 108 | it("does not change when an invalid string is used for background color", () => { 109 | verifyRuntimeResult(` 110 | TextWindow.BackgroundColor = "Red" 111 | TextWindow.WriteLine(TextWindow.BackgroundColor) 112 | TextWindow.BackgroundColor = "invalid" 113 | TextWindow.WriteLine(TextWindow.BackgroundColor) 114 | `, 115 | [], 116 | [ 117 | "Red", 118 | "Red" 119 | ]); 120 | }); 121 | 122 | it("does not change when an invalid number is used for foreground color", () => { 123 | verifyRuntimeResult(` 124 | TextWindow.ForegroundColor = "Red" 125 | TextWindow.WriteLine(TextWindow.ForegroundColor) 126 | TextWindow.ForegroundColor = 9455 127 | TextWindow.WriteLine(TextWindow.ForegroundColor) 128 | `, 129 | [], 130 | [ 131 | "Red", 132 | "Red" 133 | ]); 134 | }); 135 | 136 | it("does not change when an invalid string is used for foreground color", () => { 137 | verifyRuntimeResult(` 138 | TextWindow.ForegroundColor = "Red" 139 | TextWindow.WriteLine(TextWindow.ForegroundColor) 140 | TextWindow.ForegroundColor = "invalid" 141 | TextWindow.WriteLine(TextWindow.ForegroundColor) 142 | `, 143 | [], 144 | [ 145 | "Red", 146 | "Red" 147 | ]); 148 | }); 149 | 150 | it("writes partial chunks", () => { 151 | verifyRuntimeResult(` 152 | TextWindow.Write("1") 153 | TextWindow.Write("2") 154 | TextWindow.Write("3")`, 155 | [], 156 | [ 157 | "123" 158 | ]); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /src/app/components/common/shapes/shapes-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as Konva from "konva"; 2 | import { BaseShape, ShapeKind } from "./base-shape"; 3 | import { RectangleShape } from "./rectangle"; 4 | import { EllipseShape } from "./ellipse"; 5 | import { TriangleShape } from "./triangle"; 6 | import { LineShape } from "./line"; 7 | import { TextShape } from "./text"; 8 | import { IShapesLibraryPlugin } from "../../../../compiler/runtime/libraries/shapes"; 9 | 10 | export class ShapesLibraryPlugin implements IShapesLibraryPlugin { 11 | private readonly shapes: { [name: string]: BaseShape } = {}; 12 | 13 | public constructor( 14 | private readonly layer: Konva.Layer, 15 | private readonly stage: Konva.Stage) { 16 | } 17 | 18 | public addRectangle(width: number, height: number): string { 19 | return this.addShape(new RectangleShape(width, height)); 20 | } 21 | 22 | public addEllipse(width: number, height: number): string { 23 | return this.addShape(new EllipseShape(width, height)); 24 | } 25 | 26 | public addTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): string { 27 | return this.addShape(new TriangleShape(x1, y1, x2, y2, x3, y3)); 28 | } 29 | 30 | public addLine(x1: number, y1: number, x2: number, y2: number): string { 31 | return this.addShape(new LineShape(x1, y1, x2, y2)); 32 | } 33 | 34 | public addImage(): string { 35 | throw new Error("TODO: Not Implemented Yet"); 36 | } 37 | 38 | public addText(text: string): string { 39 | return this.addShape(new TextShape(text)); 40 | } 41 | 42 | public setText(shapeName: string, text: string): void { 43 | const shape = this.shapes[shapeName]; 44 | if (shape && shape.kind === ShapeKind.Text) { 45 | (shape as TextShape).setText(text); 46 | this.stage.draw(); 47 | } 48 | } 49 | 50 | public remove(shapeName: string): void { 51 | const shape = this.shapes[shapeName]; 52 | if (shape) { 53 | shape.remove(); 54 | delete (this.shapes[shapeName]); 55 | this.stage.draw(); 56 | } 57 | } 58 | 59 | public move(shapeName: string, x: number, y: number): void { 60 | const shape = this.shapes[shapeName]; 61 | if (shape) { 62 | shape.move(x, y); 63 | this.stage.draw(); 64 | } 65 | } 66 | 67 | public rotate(shapeName: string, angle: number): void { 68 | const shape = this.shapes[shapeName]; 69 | if (shape) { 70 | shape.rotate(angle); 71 | this.stage.draw(); 72 | } 73 | } 74 | 75 | public zoom(shapeName: string, scaleX: number, scaleY: number): void { 76 | const shape = this.shapes[shapeName]; 77 | if (shape) { 78 | shape.zoom(scaleX, scaleY); 79 | this.stage.draw(); 80 | } 81 | } 82 | 83 | public animate(shapeName: string, x: number, y: number, duration: number): void { 84 | const shape = this.shapes[shapeName]; 85 | if (shape) { 86 | shape.animate(x, y, duration, this.layer); 87 | this.stage.draw(); 88 | } 89 | } 90 | 91 | public getLeft(shapeName: string): number { 92 | const shape = this.shapes[shapeName]; 93 | if (shape) { 94 | return shape.getLeft(); 95 | } 96 | 97 | return 0; 98 | } 99 | 100 | public getTop(shapeName: string): number { 101 | const shape = this.shapes[shapeName]; 102 | if (shape) { 103 | return shape.getTop(); 104 | } 105 | 106 | return 0; 107 | } 108 | 109 | public getOpacity(shapeName: string): number { 110 | const shape = this.shapes[shapeName]; 111 | if (shape) { 112 | return this.shapes[shapeName].getOpacity() * 100; 113 | } 114 | 115 | return 0; 116 | } 117 | 118 | public setOpacity(shapeName: string, level: number): void { 119 | const shape = this.shapes[shapeName]; 120 | if (shape) { 121 | if (level < 0) { 122 | level = 0; 123 | } 124 | else if (level > 100) { 125 | level = 100; 126 | } 127 | 128 | shape.setOpacity(level / 100); 129 | this.stage.draw(); 130 | } 131 | } 132 | 133 | public setVisibility(shapeName: string, isVisible: boolean): void { 134 | const shape = this.shapes[shapeName]; 135 | if (shape) { 136 | if (isVisible) { 137 | shape.showShape(); 138 | } 139 | else { 140 | shape.hideShape(); 141 | } 142 | 143 | this.stage.draw(); 144 | } 145 | } 146 | 147 | private addShape(shape: BaseShape): string { 148 | this.shapes[shape.name] = shape; 149 | this.layer.add(shape.instance); 150 | this.stage.draw(); 151 | 152 | return shape.name; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/compiler/runtime/libraries/text-window.ts: -------------------------------------------------------------------------------- 1 | import { LibraryTypeInstance, LibraryMethodInstance, LibraryPropertyInstance, LibraryEventInstance } from "../libraries"; 2 | import { ValueKind, BaseValue } from "../values/base-value"; 3 | import { StringValue } from "../values/string-value"; 4 | import { NumberValue } from "../values/number-value"; 5 | import { ExecutionState, ExecutionEngine } from "../../execution-engine"; 6 | 7 | export enum TextWindowColor { 8 | Black = 0, 9 | DarkBlue = 1, 10 | DarkGreen = 2, 11 | DarkCyan = 3, 12 | DarkRed = 4, 13 | DarkMagenta = 5, 14 | DarkYellow = 6, 15 | Gray = 7, 16 | DarkGray = 8, 17 | Blue = 9, 18 | Green = 10, 19 | Cyan = 11, 20 | Red = 12, 21 | Magenta = 13, 22 | Yellow = 14, 23 | White = 15 24 | } 25 | 26 | export interface ITextWindowLibraryPlugin { 27 | inputIsNeeded(kind: ValueKind): void; 28 | 29 | checkInputBuffer(): BaseValue | undefined; 30 | writeText(value: string, appendNewLine: boolean): void; 31 | 32 | getForegroundColor(): TextWindowColor; 33 | setForegroundColor(color: TextWindowColor): void; 34 | getBackgroundColor(): TextWindowColor; 35 | setBackgroundColor(color: TextWindowColor): void; 36 | } 37 | 38 | export class TextWindowLibrary implements LibraryTypeInstance { 39 | private _pluginInstance: ITextWindowLibraryPlugin | undefined; 40 | 41 | public get plugin(): ITextWindowLibraryPlugin { 42 | if (!this._pluginInstance) { 43 | throw new Error("Plugin is not set."); 44 | } 45 | 46 | return this._pluginInstance; 47 | } 48 | 49 | public set plugin(plugin: ITextWindowLibraryPlugin) { 50 | this._pluginInstance = plugin; 51 | } 52 | 53 | private executeReadMethod(engine: ExecutionEngine, kind: ValueKind): void { 54 | const bufferValue = this.plugin.checkInputBuffer(); 55 | if (bufferValue) { 56 | if (bufferValue.kind !== kind) { 57 | throw new Error(`Expecting input kind '${ValueKind[kind]}' but buffer has kind '${ValueKind[bufferValue.kind]}'`); 58 | } 59 | 60 | engine.pushEvaluationStack(bufferValue); 61 | engine.state = ExecutionState.Running; 62 | } else { 63 | engine.state = ExecutionState.BlockedOnInput; 64 | this.plugin.inputIsNeeded(kind); 65 | } 66 | } 67 | 68 | private executeWriteMethod(engine: ExecutionEngine, appendNewLine: boolean): void { 69 | const value = engine.popEvaluationStack().toValueString(); 70 | this.plugin.writeText(value, appendNewLine); 71 | } 72 | 73 | private tryParseColorValue(value: BaseValue): TextWindowColor | undefined { 74 | switch (value.kind) { 75 | case ValueKind.Number: { 76 | const numberValue = (value as NumberValue).value; 77 | if (TextWindowColor[numberValue]) { 78 | return numberValue; 79 | } 80 | break; 81 | } 82 | case ValueKind.String: { 83 | const stringValue = (value as StringValue).value.toLowerCase(); 84 | for (let color in TextWindowColor) { 85 | if (color.toLowerCase() === stringValue) { 86 | return TextWindowColor[color]; 87 | } 88 | } 89 | break; 90 | } 91 | } 92 | 93 | return undefined; 94 | } 95 | 96 | private setForegroundColor(value: BaseValue): void { 97 | const color = this.tryParseColorValue(value); 98 | if (color) { 99 | this.plugin.setForegroundColor(color); 100 | } 101 | } 102 | 103 | private setBackgroundColor(value: BaseValue): void { 104 | const color = this.tryParseColorValue(value); 105 | if (color) { 106 | this.plugin.setBackgroundColor(color); 107 | } 108 | } 109 | 110 | private getForegroundColor(): BaseValue { 111 | return new StringValue(TextWindowColor[this.plugin.getForegroundColor()]); 112 | } 113 | 114 | private getBackgroundColor(): BaseValue { 115 | return new StringValue(TextWindowColor[this.plugin.getBackgroundColor()]); 116 | } 117 | 118 | public readonly methods: { readonly [name: string]: LibraryMethodInstance } = { 119 | Read: { execute: engine => this.executeReadMethod(engine, ValueKind.String) }, 120 | ReadNumber: { execute: engine => this.executeReadMethod(engine, ValueKind.Number) }, 121 | Write: { execute: engine => this.executeWriteMethod(engine, false) }, 122 | WriteLine: { execute: engine => this.executeWriteMethod(engine, true) } 123 | }; 124 | 125 | public readonly properties: { readonly [name: string]: LibraryPropertyInstance } = { 126 | ForegroundColor: { getter: this.getForegroundColor.bind(this), setter: this.setForegroundColor.bind(this) }, 127 | BackgroundColor: { getter: this.getBackgroundColor.bind(this), setter: this.setBackgroundColor.bind(this) } 128 | }; 129 | 130 | public readonly events: { readonly [name: string]: LibraryEventInstance } = {}; 131 | } 132 | -------------------------------------------------------------------------------- /src/compiler/runtime/values/string-value.ts: -------------------------------------------------------------------------------- 1 | import { NumberValue } from "./number-value"; 2 | import { ExecutionEngine } from "../../execution-engine"; 3 | import { AddInstruction, DivideInstruction, MultiplyInstruction, SubtractInstruction } from "../../emitting/instructions"; 4 | import { BaseValue, ValueKind, Constants } from "./base-value"; 5 | import { TokenKind } from "../../syntax/tokens"; 6 | import { ErrorCode, Diagnostic } from "../../utils/diagnostics"; 7 | import { CompilerUtils } from "../../utils/compiler-utils"; 8 | 9 | export class StringValue extends BaseValue { 10 | public constructor(public readonly value: string) { 11 | super(); 12 | } 13 | 14 | public toBoolean(): boolean { 15 | return this.value.toLowerCase() === Constants.True.toLowerCase(); 16 | } 17 | 18 | public toDebuggerString(): string { 19 | return `"${this.value.toString()}"`; 20 | } 21 | 22 | public toValueString(): string { 23 | return this.value; 24 | } 25 | 26 | public get kind(): ValueKind { 27 | return ValueKind.String; 28 | } 29 | 30 | public tryConvertToNumber(): BaseValue { 31 | const number = parseFloat(this.value.trim()); 32 | if (isNaN(number)) { 33 | return this; 34 | } else { 35 | return new NumberValue(number); 36 | } 37 | } 38 | 39 | public isEqualTo(other: BaseValue): boolean { 40 | switch (other.kind) { 41 | case ValueKind.String: 42 | return this.value === (other as StringValue).value; 43 | case ValueKind.Number: 44 | return this.value.trim() === (other as NumberValue).value.toString(); 45 | case ValueKind.Array: 46 | return false; 47 | default: 48 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 49 | } 50 | } 51 | 52 | public isLessThan(other: BaseValue): boolean { 53 | const thisConverted = this.tryConvertToNumber(); 54 | if (thisConverted.tryConvertToNumber().kind === ValueKind.String) { 55 | return false; 56 | } else { 57 | return thisConverted.isLessThan(other); 58 | } 59 | } 60 | 61 | public isGreaterThan(other: BaseValue): boolean { 62 | const thisConverted = this.tryConvertToNumber(); 63 | if (thisConverted.tryConvertToNumber().kind === ValueKind.String) { 64 | return false; 65 | } else { 66 | return thisConverted.isGreaterThan(other); 67 | } 68 | } 69 | 70 | public add(other: BaseValue, engine: ExecutionEngine, instruction: AddInstruction): BaseValue { 71 | const thisConverted = this.tryConvertToNumber(); 72 | if (thisConverted.tryConvertToNumber().kind !== ValueKind.String) { 73 | return thisConverted.add(other, engine, instruction); 74 | } 75 | 76 | other = other.tryConvertToNumber(); 77 | switch (other.kind) { 78 | case ValueKind.String: 79 | return new StringValue(this.value + (other as StringValue).value); 80 | case ValueKind.Number: 81 | return new StringValue(this.value + (other as NumberValue).value.toString()); 82 | case ValueKind.Array: 83 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Plus))); 84 | return this; 85 | default: 86 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 87 | } 88 | } 89 | 90 | public subtract(other: BaseValue, engine: ExecutionEngine, instruction: SubtractInstruction): BaseValue { 91 | const thisConverted = this.tryConvertToNumber(); 92 | if (thisConverted.tryConvertToNumber().kind === ValueKind.String) { 93 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Minus))); 94 | return this; 95 | } else { 96 | return thisConverted.subtract(other, engine, instruction); 97 | } 98 | } 99 | 100 | public multiply(other: BaseValue, engine: ExecutionEngine, instruction: MultiplyInstruction): BaseValue { 101 | const thisConverted = this.tryConvertToNumber(); 102 | if (thisConverted.tryConvertToNumber().kind === ValueKind.String) { 103 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Multiply))); 104 | return this; 105 | } else { 106 | return thisConverted.multiply(other, engine, instruction); 107 | } 108 | } 109 | 110 | public divide(other: BaseValue, engine: ExecutionEngine, instruction: DivideInstruction): BaseValue { 111 | const thisConverted = this.tryConvertToNumber(); 112 | if (thisConverted.tryConvertToNumber().kind === ValueKind.String) { 113 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Divide))); 114 | return this; 115 | } else { 116 | return thisConverted.divide(other, engine, instruction); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/compiler/runtime/libraries-metadata.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { CompilerUtils } from "../../../src/compiler/utils/compiler-utils"; 3 | import { RuntimeLibraries } from "../../../src/compiler/runtime/libraries"; 4 | import { verifyCompilationErrors } from "../helpers"; 5 | 6 | describe("Compiler.Runtime.LibrariesMetadata", () => { 7 | it("all libraries have correct metadata", () => { 8 | const implementations = new RuntimeLibraries(); 9 | 10 | CompilerUtils.values(RuntimeLibraries.Metadata).forEach(library => { 11 | const libraryImplementation = implementations[library.typeName]; 12 | expect(libraryImplementation).toBeDefined(); 13 | 14 | expect(library.typeName.length).toBeGreaterThan(0); 15 | expect(library.description.length).toBeGreaterThan(0); 16 | 17 | CompilerUtils.values(library.methods).forEach(method => { 18 | expect(method.typeName.length).toBeGreaterThan(0); 19 | expect(method.methodName.length).toBeGreaterThan(0); 20 | expect(method.description.length).toBeGreaterThan(0); 21 | expect(method.returnsValue).toBeDefined(); 22 | 23 | method.parameters.forEach(parameter => { 24 | expect(parameter.length).toBeGreaterThan(0); 25 | expect(method.parameterDescription(parameter).length).toBeGreaterThan(0); 26 | }); 27 | 28 | expect(libraryImplementation.methods[method.methodName]).toBeDefined(); 29 | delete (libraryImplementation.methods)[method.methodName]; 30 | }); 31 | 32 | expect(Object.keys(libraryImplementation.methods).length).toBe(0); 33 | 34 | CompilerUtils.values(library.properties).forEach(property => { 35 | expect(property.typeName.length).toBeGreaterThan(0); 36 | expect(property.propertyName.length).toBeGreaterThan(0); 37 | expect(property.description.length).toBeGreaterThan(0); 38 | 39 | const propertyImplementation = libraryImplementation.properties[property.propertyName]; 40 | expect(property.hasGetter).toBe(!!propertyImplementation.getter); 41 | expect(property.hasSetter).toBe(!!propertyImplementation.setter); 42 | 43 | delete (libraryImplementation.properties)[property.propertyName]; 44 | }); 45 | 46 | expect(Object.keys(libraryImplementation.properties).length).toBe(0); 47 | 48 | CompilerUtils.values(library.events).forEach(event => { 49 | expect(event.typeName.length).toBeGreaterThan(0); 50 | expect(event.eventName.length).toBeGreaterThan(0); 51 | expect(event.description.length).toBeGreaterThan(0); 52 | 53 | const eventImplementation = libraryImplementation.events[event.eventName]; 54 | expect(eventImplementation.setSubModule).toBeDefined(); 55 | expect(eventImplementation.raise).toBeDefined(); 56 | 57 | delete (libraryImplementation.events)[event.eventName]; 58 | }); 59 | 60 | expect(Object.keys(libraryImplementation.events).length).toBe(0); 61 | 62 | delete (implementations)[library.typeName]; 63 | }); 64 | 65 | expect(Object.keys(implementations).length).toBe(0); 66 | }); 67 | 68 | it("has no kind if there were no library calls in code", () => { 69 | const compilation = verifyCompilationErrors(` 70 | x = 0`); 71 | 72 | expect(compilation.kind.writesToTextWindow()).toBe(false); 73 | expect(compilation.kind.drawsShapes()).toBe(false); 74 | }); 75 | 76 | it("has writesToText kind if there were calls to TextWindow", () => { 77 | const compilation = verifyCompilationErrors(` 78 | x = 0 79 | TextWindow.WriteLine(x)`); 80 | 81 | expect(compilation.kind.writesToTextWindow()).toBe(true); 82 | expect(compilation.kind.drawsShapes()).toBe(false); 83 | }); 84 | 85 | /* TODO: reenable 86 | it("has drawsShapes kind if there were calls to Turtle", () => { 87 | const compilation = verifyCompilationErrors(` 88 | Turtle.TurnLeft()`); 89 | 90 | expect(compilation.kind.writesToTextWindow()).toBe(false); 91 | expect(compilation.kind.drawsShapes()).toBe(true); 92 | }); 93 | 94 | it("has drawsShapes kind if there were calls to Controls", () => { 95 | const compilation = verifyCompilationErrors(` 96 | Controls.AddButton("value", 0, 0)`); 97 | 98 | expect(compilation.kind.writesToTextWindow()).toBe(false); 99 | expect(compilation.kind.drawsShapes()).toBe(true); 100 | }); 101 | */ 102 | 103 | it("has drawsShapes kind if there were calls to Shapes", () => { 104 | const compilation = verifyCompilationErrors(` 105 | Shapes.AddRectangle( 0, 0)`); 106 | 107 | expect(compilation.kind.writesToTextWindow()).toBe(false); 108 | expect(compilation.kind.drawsShapes()).toBe(true); 109 | }); 110 | 111 | it("has both kinds if there were calls to Shapes and TextWindow", () => { 112 | const compilation = verifyCompilationErrors(` 113 | shape = Shapes.AddRectangle( 0, 0) 114 | TextWindow.WriteLine(shape)`); 115 | 116 | expect(compilation.kind.writesToTextWindow()).toBe(true); 117 | expect(compilation.kind.drawsShapes()).toBe(true); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/addition.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult, verifyRuntimeError } from "../../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Runtime.Expressions.Addition", () => { 7 | it("computes addition - number plus number", () => { 8 | verifyRuntimeResult(` 9 | TextWindow.WriteLine(1 + 4)`, 10 | [], 11 | ["5"]); 12 | }); 13 | 14 | it("computes addition - number plus numeric string", () => { 15 | verifyRuntimeResult(` 16 | TextWindow.WriteLine(1 + "7")`, 17 | [], 18 | ["8"]); 19 | }); 20 | 21 | it("computes addition - number plus non-numeric string", () => { 22 | verifyRuntimeResult(` 23 | TextWindow.WriteLine(1 + "t")`, 24 | [], 25 | ["1t"]); 26 | }); 27 | 28 | it("computes addition - number plus array - error", () => { 29 | verifyRuntimeError(` 30 | x[0] = 1 31 | TextWindow.WriteLine(1 + x)`, 32 | // TextWindow.WriteLine(1 + x) 33 | // ^^^^^ 34 | // You cannot use the operator '+' with an array value 35 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 26), "+")); 36 | }); 37 | 38 | it("computes addition - numeric string plus number", () => { 39 | verifyRuntimeResult(` 40 | TextWindow.WriteLine("1" + 4)`, 41 | [], 42 | ["5"]); 43 | }); 44 | 45 | it("computes addition - numeric string plus numeric string", () => { 46 | verifyRuntimeResult(` 47 | TextWindow.WriteLine("1" + "7")`, 48 | [], 49 | ["8"]); 50 | }); 51 | 52 | it("computes addition - numeric string plus non-numeric string", () => { 53 | verifyRuntimeResult(` 54 | TextWindow.WriteLine("1" + "t")`, 55 | [], 56 | ["1t"]); 57 | }); 58 | 59 | it("computes addition - numeric string plus array - error", () => { 60 | verifyRuntimeError(` 61 | x[0] = 1 62 | TextWindow.WriteLine("1" + x)`, 63 | // TextWindow.WriteLine("1" + x) 64 | // ^^^^^^^ 65 | // You cannot use the operator '+' with an array value 66 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "+")); 67 | }); 68 | 69 | it("computes addition - non-numeric string plus number", () => { 70 | verifyRuntimeResult(` 71 | TextWindow.WriteLine("r" + 5)`, 72 | [], 73 | ["r5"]); 74 | }); 75 | 76 | it("computes addition - non-numeric string plus numeric string", () => { 77 | verifyRuntimeResult(` 78 | TextWindow.WriteLine("r" + "4")`, 79 | [], 80 | ["r4"]); 81 | }); 82 | 83 | it("computes addition - non-numeric string plus non-numeric string", () => { 84 | verifyRuntimeResult(` 85 | TextWindow.WriteLine("r" + "t")`, 86 | [], 87 | ["rt"]); 88 | }); 89 | 90 | it("computes addition - non-numeric string plus array - error", () => { 91 | verifyRuntimeError(` 92 | x[0] = 1 93 | TextWindow.WriteLine("r" + x)`, 94 | // TextWindow.WriteLine("r" + x) 95 | // ^^^^^^^ 96 | // You cannot use the operator '+' with an array value 97 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "+")); 98 | }); 99 | 100 | it("computes addition - array plus number", () => { 101 | verifyRuntimeError(` 102 | x[0] = 1 103 | TextWindow.WriteLine(x + 5)`, 104 | // TextWindow.WriteLine(x + 5) 105 | // ^^^^^ 106 | // You cannot use the operator '+' with an array value 107 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 26), "+")); 108 | }); 109 | 110 | it("computes addition - array plus numeric string", () => { 111 | verifyRuntimeError(` 112 | x[0] = 1 113 | TextWindow.WriteLine(x + "4")`, 114 | // TextWindow.WriteLine(x + "4") 115 | // ^^^^^^^ 116 | // You cannot use the operator '+' with an array value 117 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "+")); 118 | }); 119 | 120 | it("computes addition - array plus non-numeric string", () => { 121 | verifyRuntimeError(` 122 | x[0] = 1 123 | TextWindow.WriteLine(x + "t")`, 124 | // TextWindow.WriteLine(x + "t") 125 | // ^^^^^^^ 126 | // You cannot use the operator '+' with an array value 127 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "+")); 128 | }); 129 | 130 | it("computes addition - array plus array - error", () => { 131 | verifyRuntimeError(` 132 | x[0] = 1 133 | y[0] = 1 134 | TextWindow.WriteLine(x + y)`, 135 | // TextWindow.WriteLine(x + y) 136 | // ^^^^^ 137 | // You cannot use the operator '+' with an array value 138 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(3, 21, 3, 26), "+")); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/compiler/execution-engine.ts: -------------------------------------------------------------------------------- 1 | import { BaseValue } from "./runtime/values/base-value"; 2 | import { Compilation } from "./compilation"; 3 | import { BaseInstruction } from "./emitting/instructions"; 4 | import { RuntimeLibraries } from "./runtime/libraries"; 5 | import { Diagnostic } from "./utils/diagnostics"; 6 | import { ArrayValue } from "./runtime/values/array-value"; 7 | import { PubSubPayloadChannel } from "./utils/notifications"; 8 | import { ModulesBinder } from "./binding/modules-binder"; 9 | 10 | export interface StackFrame { 11 | moduleName: string; 12 | instructionIndex: number; 13 | } 14 | 15 | export enum ExecutionMode { 16 | RunToEnd, 17 | Debug, 18 | NextStatement 19 | } 20 | 21 | export enum ExecutionState { 22 | Running, 23 | Paused, 24 | BlockedOnInput, 25 | Terminated 26 | } 27 | 28 | export class ExecutionEngine { 29 | private _libraries: RuntimeLibraries = new RuntimeLibraries(); 30 | private _executionStack: StackFrame[] = []; 31 | private _evaluationStack: BaseValue[] = []; 32 | private _memory: ArrayValue = new ArrayValue(); 33 | private _modules: { readonly [name: string]: ReadonlyArray }; 34 | 35 | private _exception?: Diagnostic; 36 | private _currentLine: number = 0; 37 | private _state: ExecutionState = ExecutionState.Running; 38 | 39 | public readonly programTerminated: PubSubPayloadChannel = new PubSubPayloadChannel("programTerminated"); 40 | 41 | public get libraries(): RuntimeLibraries { 42 | return this._libraries; 43 | } 44 | 45 | public get executionStack(): ReadonlyArray { 46 | return this._executionStack; 47 | } 48 | 49 | public get evaluationStack(): ReadonlyArray { 50 | return this._evaluationStack; 51 | } 52 | 53 | public get memory(): ArrayValue { 54 | return this._memory; 55 | } 56 | 57 | public get modules(): { readonly [name: string]: ReadonlyArray } { 58 | return this._modules; 59 | } 60 | 61 | public get exception(): Diagnostic | undefined { 62 | return this._exception; 63 | } 64 | 65 | public get state(): ExecutionState { 66 | return this._state; 67 | } 68 | 69 | public set state(newState: ExecutionState) { 70 | this._state = newState; 71 | } 72 | 73 | public constructor(compilation: Compilation) { 74 | if (compilation.diagnostics.length) { 75 | throw new Error(`Cannot execute a compilation with errors`); 76 | } 77 | 78 | this._modules = compilation.emit(); 79 | 80 | this._executionStack.push({ 81 | moduleName: ModulesBinder.MainModuleName, 82 | instructionIndex: 0 83 | }); 84 | } 85 | 86 | public execute(mode: ExecutionMode): void { 87 | if (this._state === ExecutionState.Paused) { 88 | this._state = ExecutionState.Running; 89 | } 90 | 91 | while (true) { 92 | if (this._state === ExecutionState.Terminated) { 93 | return; 94 | } 95 | 96 | if (this._executionStack.length === 0) { 97 | this.terminate(); 98 | return; 99 | } 100 | 101 | const frame = this._executionStack[this._executionStack.length - 1]; 102 | if (frame.instructionIndex === this._modules[frame.moduleName].length) { 103 | this._executionStack.pop(); 104 | continue; 105 | } 106 | 107 | const instruction = this._modules[frame.moduleName][frame.instructionIndex]; 108 | if (instruction.sourceRange.start.line !== this._currentLine && mode === ExecutionMode.NextStatement) { 109 | this._currentLine = instruction.sourceRange.start.line; 110 | this._state = ExecutionState.Paused; 111 | return; 112 | } 113 | 114 | instruction.execute(this, mode, frame); 115 | 116 | switch (this.state) { 117 | case ExecutionState.Running: 118 | break; 119 | case ExecutionState.Paused: 120 | case ExecutionState.Terminated: 121 | case ExecutionState.BlockedOnInput: 122 | return; 123 | default: 124 | throw new Error(`Unexpected execution state: '${ExecutionState[this.state]}'`); 125 | } 126 | } 127 | } 128 | 129 | public terminate(exception?: Diagnostic): void { 130 | this._state = ExecutionState.Terminated; 131 | this._exception = exception; 132 | this.programTerminated.publish(exception); 133 | } 134 | 135 | public popEvaluationStack(): BaseValue { 136 | const value = this._evaluationStack.pop(); 137 | if (value) { 138 | return value; 139 | } 140 | 141 | throw new Error("Evaluation stack empty"); 142 | } 143 | 144 | public pushEvaluationStack(value: BaseValue): void { 145 | this._evaluationStack.push(value); 146 | } 147 | 148 | public pushSubModule(name: string): void { 149 | if (this._modules[name]) { 150 | this._executionStack.push({ 151 | moduleName: name, 152 | instructionIndex: 0 153 | }); 154 | } else { 155 | throw new Error(`SubModule ${name} not found`); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of SmallBasic Online is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in SmallBasic Online to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | 56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. . 57 | 58 | 59 | 60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 61 | 62 | ## 7. Addressing Grievances 63 | 64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify the SmallBasic council with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 65 | 66 | 67 | 68 | ## 8. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 9. Contact info 75 | 76 | 77 | 78 | ## 10. License and attribution 79 | 80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | 84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) 85 | -------------------------------------------------------------------------------- /src/app/components/editor/documentation/index.tsx: -------------------------------------------------------------------------------- 1 | import { RuntimeLibraries } from "../../../../compiler/runtime/libraries"; 2 | import * as React from "react"; 3 | import { EditorResources } from "../../../../strings/editor"; 4 | 5 | import "./style.css"; 6 | import { CompilerUtils } from "../../../../compiler/utils/compiler-utils"; 7 | 8 | const DocumentationIcon = require("./header.png"); 9 | 10 | interface DocumentationProps { 11 | } 12 | 13 | interface DocumentationState { 14 | library?: string; 15 | member?: string; 16 | } 17 | 18 | export class DocumentationComponent extends React.Component { 19 | 20 | public constructor(props: DocumentationProps) { 21 | super(props); 22 | this.state = { 23 | library: undefined, 24 | member: undefined 25 | }; 26 | } 27 | 28 | private libraryClicked(name: string): void { 29 | if (this.state.library === name) { 30 | this.setState({ 31 | library: undefined, 32 | member: undefined 33 | }); 34 | } else { 35 | this.setState({ 36 | library: name, 37 | member: undefined 38 | }); 39 | } 40 | } 41 | 42 | private memberClicked(name: string): void { 43 | if (this.state.member === name) { 44 | this.setState({ 45 | library: this.state.library, 46 | member: undefined 47 | }); 48 | } else { 49 | this.setState({ 50 | library: this.state.library, 51 | member: name 52 | }); 53 | } 54 | } 55 | 56 | public render(): JSX.Element { 57 | return ( 58 |
59 |
60 |
{EditorResources.Documentation_Header}
61 |
    62 | {CompilerUtils.values(RuntimeLibraries.Metadata).map(library => { 63 | return ( 64 |
  • 65 |

    this.libraryClicked(library.typeName)).bind(this)}>{library.typeName}

    66 |
    67 |

    {library.description}

    68 |
      69 | {CompilerUtils.values(library.properties).map(property => { 70 | return ( 71 |
    • 72 |

      this.memberClicked(property.propertyName)).bind(this)}> 73 | {library.typeName}.{property.propertyName} 74 |

      75 |
      76 |

      {property.description}

      77 |
      78 |
    • 79 | ); 80 | })} 81 | {CompilerUtils.values(library.methods).map(method => { 82 | return ( 83 |
    • 84 |

      this.memberClicked(method.methodName)).bind(this)}> 85 | {method.typeName}.{method.methodName}({method.parameters.join(", ")}) 86 |

      87 |
      88 |

      {method.description}

      89 |
        90 | {method.parameters.map(parameter => 91 |
      • 92 |

        {parameter}: {method.parameterDescription(parameter)}

        93 |
      • 94 | )} 95 |
      96 |
      97 |
    • 98 | ); 99 | })} 100 |
    101 |
    102 |
  • 103 | ); 104 | })} 105 |
106 |
107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/compiler/runtime/expressions/subtraction.ts: -------------------------------------------------------------------------------- 1 | import "jasmine"; 2 | import { verifyRuntimeResult, verifyRuntimeError } from "../../helpers"; 3 | import { Diagnostic, ErrorCode } from "../../../../src/compiler/utils/diagnostics"; 4 | import { CompilerRange } from "../../../../src/compiler/syntax/ranges"; 5 | 6 | describe("Compiler.Runtime.Expressions.Subtraction", () => { 7 | it("computes subtraction - number minus number", () => { 8 | verifyRuntimeResult(` 9 | TextWindow.WriteLine(7 - 4)`, 10 | [], 11 | ["3"]); 12 | }); 13 | 14 | it("computes subtraction - number minus numeric string", () => { 15 | verifyRuntimeResult(` 16 | TextWindow.WriteLine(7 - "2")`, 17 | [], 18 | ["5"]); 19 | }); 20 | 21 | it("computes subtraction - number minus non-numeric string", () => { 22 | verifyRuntimeError(` 23 | TextWindow.WriteLine(1 - "t")`, 24 | // TextWindow.WriteLine(1 - "t") 25 | // ^^^^^^^ 26 | // You cannot use the operator '-' with a string value 27 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(1, 21, 1, 28), "-")); 28 | }); 29 | 30 | it("computes subtraction - number minus array - error", () => { 31 | verifyRuntimeError(` 32 | x[0] = 1 33 | TextWindow.WriteLine(1 - x)`, 34 | // TextWindow.WriteLine(1 - x) 35 | // ^^^^^ 36 | // You cannot use the operator '-' with an array value 37 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 26), "-")); 38 | }); 39 | 40 | it("computes subtraction - numeric string minus number", () => { 41 | verifyRuntimeResult(` 42 | TextWindow.WriteLine("6" - 4)`, 43 | [], 44 | ["2"]); 45 | }); 46 | 47 | it("computes subtraction - numeric string minus numeric string", () => { 48 | verifyRuntimeResult(` 49 | TextWindow.WriteLine("1" - "7")`, 50 | [], 51 | ["-6"]); 52 | }); 53 | 54 | it("computes subtraction - numeric string minus non-numeric string", () => { 55 | verifyRuntimeError(` 56 | TextWindow.WriteLine("1" - "t")`, 57 | // TextWindow.WriteLine("1" - "t") 58 | // ^^^^^^^^^ 59 | // You cannot use the operator '-' with a string value 60 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(1, 21, 1, 30), "-")); 61 | }); 62 | 63 | it("computes subtraction - numeric string minus array - error", () => { 64 | verifyRuntimeError(` 65 | x[0] = 1 66 | TextWindow.WriteLine("1" - x)`, 67 | // TextWindow.WriteLine("1" - x) 68 | // ^^^^^^^ 69 | // You cannot use the operator '-' with an array value 70 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "-")); 71 | }); 72 | 73 | it("computes subtraction - non-numeric string minus number", () => { 74 | verifyRuntimeError(` 75 | TextWindow.WriteLine("r" - 5)`, 76 | // TextWindow.WriteLine("r" - 5) 77 | // ^^^^^^^ 78 | // You cannot use the operator '-' with a string value 79 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(1, 21, 1, 28), "-")); 80 | }); 81 | 82 | it("computes subtraction - non-numeric string minus numeric string", () => { 83 | verifyRuntimeError(` 84 | TextWindow.WriteLine("r" - "4")`, 85 | // TextWindow.WriteLine("r" - "4") 86 | // ^^^^^^^^^ 87 | // You cannot use the operator '-' with a string value 88 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(1, 21, 1, 30), "-")); 89 | }); 90 | 91 | it("computes subtraction - non-numeric string minus non-numeric string", () => { 92 | verifyRuntimeError(` 93 | TextWindow.WriteLine("r" - "t")`, 94 | // TextWindow.WriteLine("r" - "t") 95 | // ^^^^^^^^^ 96 | // You cannot use the operator '-' with a string value 97 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(1, 21, 1, 30), "-")); 98 | }); 99 | 100 | it("computes subtraction - non-numeric string minus array - error", () => { 101 | verifyRuntimeError(` 102 | x[0] = 1 103 | TextWindow.WriteLine("r" - x)`, 104 | // TextWindow.WriteLine("r" - x) 105 | // ^^^^^^^ 106 | // You cannot use the operator '-' with a string value 107 | new Diagnostic(ErrorCode.CannotUseOperatorWithAString, CompilerRange.fromValues(2, 21, 2, 28), "-")); 108 | }); 109 | 110 | it("computes subtraction - array minus number", () => { 111 | verifyRuntimeError(` 112 | x[0] = 1 113 | TextWindow.WriteLine(x - 5)`, 114 | // TextWindow.WriteLine(x - 5) 115 | // ^^^^^ 116 | // You cannot use the operator '-' with an array value 117 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 26), "-")); 118 | }); 119 | 120 | it("computes subtraction - array minus numeric string", () => { 121 | verifyRuntimeError(` 122 | x[0] = 1 123 | TextWindow.WriteLine(x - "4")`, 124 | // TextWindow.WriteLine(x - "4") 125 | // ^^^^^^^ 126 | // You cannot use the operator '-' with an array value 127 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "-")); 128 | }); 129 | 130 | it("computes subtraction - array minus non-numeric string", () => { 131 | verifyRuntimeError(` 132 | x[0] = 1 133 | TextWindow.WriteLine(x - "t")`, 134 | // TextWindow.WriteLine(x - "t") 135 | // ^^^^^^^ 136 | // You cannot use the operator '-' with an array value 137 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(2, 21, 2, 28), "-")); 138 | }); 139 | 140 | it("computes subtraction - array minus array - error", () => { 141 | verifyRuntimeError(` 142 | x[0] = 1 143 | y[0] = 1 144 | TextWindow.WriteLine(x - y)`, 145 | // TextWindow.WriteLine(x - y) 146 | // ^^^^^ 147 | // You cannot use the operator '-' with an array value 148 | new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, CompilerRange.fromValues(3, 21, 3, 26), "-")); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/compiler/runtime/values/number-value.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEngine } from "../../execution-engine"; 2 | import { StringValue } from "./string-value"; 3 | import { AddInstruction, DivideInstruction, MultiplyInstruction, SubtractInstruction } from "../../emitting/instructions"; 4 | import { Diagnostic, ErrorCode } from "../../utils/diagnostics"; 5 | import { BaseValue, ValueKind } from "./base-value"; 6 | import { TokenKind } from "../../syntax/tokens"; 7 | import { CompilerUtils } from "../../utils/compiler-utils"; 8 | 9 | export class NumberValue extends BaseValue { 10 | public constructor(public readonly value: number) { 11 | super(); 12 | } 13 | 14 | public toBoolean(): boolean { 15 | return false; 16 | } 17 | 18 | public toDebuggerString(): string { 19 | return this.value.toString(); 20 | } 21 | 22 | public toValueString(): string { 23 | return this.toDebuggerString(); 24 | } 25 | 26 | public get kind(): ValueKind { 27 | return ValueKind.Number; 28 | } 29 | 30 | public tryConvertToNumber(): BaseValue { 31 | return this; 32 | } 33 | 34 | public isEqualTo(other: BaseValue): boolean { 35 | other = other.tryConvertToNumber(); 36 | 37 | switch (other.kind) { 38 | case ValueKind.String: 39 | return this.value.toString() === (other as StringValue).value; 40 | case ValueKind.Number: 41 | return this.value === (other as NumberValue).value; 42 | case ValueKind.Array: 43 | return false; 44 | default: 45 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 46 | } 47 | } 48 | 49 | public isLessThan(other: BaseValue): boolean { 50 | other = other.tryConvertToNumber(); 51 | 52 | switch (other.kind) { 53 | case ValueKind.String: 54 | case ValueKind.Array: 55 | return false; 56 | case ValueKind.Number: 57 | return this.value < (other as NumberValue).value; 58 | default: 59 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 60 | } 61 | } 62 | 63 | public isGreaterThan(other: BaseValue): boolean { 64 | other = other.tryConvertToNumber(); 65 | 66 | switch (other.kind) { 67 | case ValueKind.String: 68 | case ValueKind.Array: 69 | return false; 70 | case ValueKind.Number: 71 | return this.value > (other as NumberValue).value; 72 | default: 73 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 74 | } 75 | } 76 | 77 | public add(other: BaseValue, engine: ExecutionEngine, instruction: AddInstruction): BaseValue { 78 | other = other.tryConvertToNumber(); 79 | 80 | switch (other.kind) { 81 | case ValueKind.String: 82 | return new StringValue(this.value.toString() + (other as StringValue).value); 83 | case ValueKind.Number: 84 | return new NumberValue(this.value + (other as NumberValue).value); 85 | case ValueKind.Array: 86 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Plus))); 87 | return this; 88 | default: 89 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 90 | } 91 | } 92 | 93 | public subtract(other: BaseValue, engine: ExecutionEngine, instruction: SubtractInstruction): BaseValue { 94 | other = other.tryConvertToNumber(); 95 | 96 | switch (other.kind) { 97 | case ValueKind.String: 98 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Minus))); 99 | return this; 100 | case ValueKind.Number: 101 | return new NumberValue(this.value - (other as NumberValue).value); 102 | case ValueKind.Array: 103 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Minus))); 104 | return this; 105 | default: 106 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 107 | } 108 | } 109 | 110 | public multiply(other: BaseValue, engine: ExecutionEngine, instruction: MultiplyInstruction): BaseValue { 111 | other = other.tryConvertToNumber(); 112 | 113 | switch (other.kind) { 114 | case ValueKind.String: 115 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Multiply))); 116 | return this; 117 | case ValueKind.Number: 118 | return new NumberValue(this.value * (other as NumberValue).value); 119 | case ValueKind.Array: 120 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Multiply))); 121 | return this; 122 | default: 123 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 124 | } 125 | } 126 | 127 | public divide(other: BaseValue, engine: ExecutionEngine, instruction: DivideInstruction): BaseValue { 128 | other = other.tryConvertToNumber(); 129 | 130 | switch (other.kind) { 131 | case ValueKind.String: 132 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAString, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Divide))); 133 | return this; 134 | case ValueKind.Number: 135 | const otherValue = (other as NumberValue).value; 136 | if (otherValue === 0) { 137 | engine.terminate(new Diagnostic(ErrorCode.CannotDivideByZero, instruction.sourceRange)); 138 | return this; 139 | } else { 140 | return new NumberValue(this.value / otherValue); 141 | } 142 | case ValueKind.Array: 143 | engine.terminate(new Diagnostic(ErrorCode.CannotUseOperatorWithAnArray, instruction.sourceRange, CompilerUtils.tokenToDisplayString(TokenKind.Divide))); 144 | return this; 145 | default: 146 | throw new Error(`Unexpected value kind ${ValueKind[other.kind]}`); 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------