From 39e81eda6738c9eafdf69669f9e368c6df29845f Mon Sep 17 00:00:00 2001 From: Kefniark Date: Wed, 12 May 2021 19:04:22 +0900 Subject: [PATCH 1/3] Add basic code analyzer --- README.md | 4 +- package-lock.json | 84 +++++----- package.json | 11 +- samples/main.ako | 3 +- samples/{module.json => manifest.json} | 0 samples/math/{module.json => manifest.json} | 0 samples/vec/main.ako | 2 +- samples/vec/{module.json => manifest.json} | 0 src/analyzer.ts | 161 ++++++++++++++++++++ src/core.ts | 4 + src/dist/ako-cli.ts | 85 ++++++++--- src/dist/ako-node.ts | 3 +- src/dist/ako-web.ts | 3 +- src/elements/conditional.ts | 23 ++- src/elements/function.ts | 23 ++- src/elements/loop.ts | 4 +- src/elements/scalar.ts | 24 ++- src/interpreter.ts | 2 +- src/semantic.ts | 122 ++++++++------- src/std/collections/list.ts | 6 + src/std/math/index.ts | 7 +- src/std/system/sleep.ts | 6 +- tests/interpreter/analyzer.test.ts | 106 +++++++++++++ tests/std/list.test.ts | 11 ++ tests/std/math.test.ts | 21 +++ tsconfig.json | 2 +- 26 files changed, 562 insertions(+), 155 deletions(-) rename samples/{module.json => manifest.json} (100%) rename samples/math/{module.json => manifest.json} (100%) rename samples/vec/{module.json => manifest.json} (100%) create mode 100644 src/analyzer.ts create mode 100644 tests/interpreter/analyzer.test.ts diff --git a/README.md b/README.md index 7877e74..10258c5 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ Most [No-code or Low-code](https://en.wikipedia.org/wiki/Low-code_development_pl Ako tries to fill the gap by providing at the same time: * A [visual programming interface](https://github.com/ako-lang/ako-editor) for beginners -* A programming language for more advanced users but easy to learn * An interpreter designed to run almost anywhere - +* A programming language for more advanced users but easy to learn +* Automatic common error detection with code analysis and human friendly error message # Getting Started diff --git a/package-lock.json b/package-lock.json index 030550a..d3d5fdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -458,13 +458,13 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.1.tgz", - "integrity": "sha512-kVTAghWDDhsvQ602tHBc6WmQkdaYbkcTwZu+7l24jtJiYvm9l+/y/b2BZANEezxPDiX5MK2ZecE+9BFi/YJryw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.23.0.tgz", + "integrity": "sha512-tGK1y3KIvdsQEEgq6xNn1DjiFJtl+wn8JJQiETtCbdQxw1vzjXyAaIkEmO2l6Nq24iy3uZBMFQjZ6ECf1QdgGw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.22.1", - "@typescript-eslint/scope-manager": "4.22.1", + "@typescript-eslint/experimental-utils": "4.23.0", + "@typescript-eslint/scope-manager": "4.23.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", @@ -485,55 +485,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.1.tgz", - "integrity": "sha512-svYlHecSMCQGDO2qN1v477ax/IDQwWhc7PRBiwAdAMJE7GXk5stF4Z9R/8wbRkuX/5e9dHqbIWxjeOjckK3wLQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.23.0.tgz", + "integrity": "sha512-WAFNiTDnQfrF3Z2fQ05nmCgPsO5o790vOhmWKXbbYQTO9erE1/YsFot5/LnOUizLzU2eeuz6+U/81KV5/hFTGA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.22.1", - "@typescript-eslint/types": "4.22.1", - "@typescript-eslint/typescript-estree": "4.22.1", + "@typescript-eslint/scope-manager": "4.23.0", + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/typescript-estree": "4.23.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.1.tgz", - "integrity": "sha512-l+sUJFInWhuMxA6rtirzjooh8cM/AATAe3amvIkqKFeMzkn85V+eLzb1RyuXkHak4dLfYzOmF6DXPyflJvjQnw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.23.0.tgz", + "integrity": "sha512-wsvjksHBMOqySy/Pi2Q6UuIuHYbgAMwLczRl4YanEPKW5KVxI9ZzDYh3B5DtcZPQTGRWFJrfcbJ6L01Leybwug==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.22.1", - "@typescript-eslint/types": "4.22.1", - "@typescript-eslint/typescript-estree": "4.22.1", + "@typescript-eslint/scope-manager": "4.23.0", + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/typescript-estree": "4.23.0", "debug": "^4.1.1" } }, "@typescript-eslint/scope-manager": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz", - "integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz", + "integrity": "sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.1", - "@typescript-eslint/visitor-keys": "4.22.1" + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/visitor-keys": "4.23.0" } }, "@typescript-eslint/types": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz", - "integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.23.0.tgz", + "integrity": "sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz", - "integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz", + "integrity": "sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.1", - "@typescript-eslint/visitor-keys": "4.22.1", + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/visitor-keys": "4.23.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -553,12 +553,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz", - "integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz", + "integrity": "sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/types": "4.23.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -666,7 +666,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -1159,7 +1158,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1268,7 +1266,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1276,8 +1273,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "colorette": { "version": "1.2.2", @@ -2959,8 +2955,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-symbol-support-x": { "version": "1.4.2", @@ -5492,9 +5487,9 @@ "dev": true }, "prettier": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", - "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz", + "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==", "dev": true }, "process-nextick-args": { @@ -6267,7 +6262,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } diff --git a/package.json b/package.json index ecf2c63..136d06f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "package:linux": "cd dist && nexe --input=./ako-cli.js --output=./bin/ako-linux --target=linux-12.12.0", "dev": "mocha ./tests/**/*.test.ts --recursive --reporter mochawesome", "test": "run-s test:*", - "test:depencencies": "npm-check --production || echo \"Run 'npm run update' to interactively update dependencies for this project\"", + "test:depencencies": "npm-check --production --skip-unused || echo \"Run 'npm run update' to interactively update dependencies for this project\"", "test:unit-test": "mocha ./tests/**/*.test.ts --recursive", "test:lint": "eslint src/ --ext .js,.jsx,.ts,.tsx", "test:prettier": "prettier --check \"src/**/*.ts\"", @@ -57,8 +57,8 @@ "devDependencies": { "@types/mocha": "^8.2.2", "@types/node": "^14.14.41", - "@typescript-eslint/eslint-plugin": "^4.22.1", - "@typescript-eslint/parser": "^4.22.1", + "@typescript-eslint/eslint-plugin": "^4.23.0", + "@typescript-eslint/parser": "^4.23.0", "esbuild": "^0.11.20", "esbuild-register": "^2.5.0", "eslint": "^7.26.0", @@ -70,7 +70,10 @@ "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "ohm-js": "^15.5.0", - "prettier": "^2.2.1", + "prettier": "^2.3.0", "typescript": "^4.2.4" + }, + "dependencies": { + "chalk": "^4.1.1" } } diff --git a/samples/main.ako b/samples/main.ako index 79c7958..fbec12b 100644 --- a/samples/main.ako +++ b/samples/main.ako @@ -6,8 +6,9 @@ fibo15 = @fibo(15) task DelayMessage ["msg"] { @print("Hum") for i in [3,2,1] { - @sleep(1) + @sleep(250) @print(String.repeat(".", i)) + @sleep(250) } @print(msg) } diff --git a/samples/module.json b/samples/manifest.json similarity index 100% rename from samples/module.json rename to samples/manifest.json diff --git a/samples/math/module.json b/samples/math/manifest.json similarity index 100% rename from samples/math/module.json rename to samples/math/manifest.json diff --git a/samples/vec/main.ako b/samples/vec/main.ako index fbb6deb..6b3830a 100644 --- a/samples/vec/main.ako +++ b/samples/vec/main.ako @@ -1,7 +1,7 @@ for countdown in [5,4,3,2,1,0] { if countdown > 0 { @print("{countdown} !") - @sleep(0.1) + @sleep(250) } else { @print("Countdown Finish !") } diff --git a/samples/vec/module.json b/samples/vec/manifest.json similarity index 100% rename from samples/vec/module.json rename to samples/vec/manifest.json diff --git a/src/analyzer.ts b/src/analyzer.ts new file mode 100644 index 0000000..cc702a6 --- /dev/null +++ b/src/analyzer.ts @@ -0,0 +1,161 @@ +import {Command, Context, isObject, isString} from './core' +import * as AkoElement from './elements' +import {Interpreter} from './interpreter' + +declare type CommandDebug = Command & {debug: {line: number; sample: string}} +const iterate = (elements: Command[], nodes: Command[], filter: (x: Command) => boolean): Command[] => { + for (const elem of elements) { + if (filter(elem)) nodes.push(elem) + + for (const key of Object.keys(elem)) { + if (Array.isArray(elem[key])) { + iterate(elem[key], nodes, filter) + } else if (isObject(elem[key]) && 'type' in elem[key]) { + iterate([elem[key]], nodes, filter) + } + } + } + return nodes +} + +export interface AnalyzeInfo { + level: 'notice' | 'warning' | 'error' + line: number + sample_before?: string + sample_after?: string + sample?: string + message: string + file?: string +} + +export class Analyzer { + interpreter: Interpreter + + constructor(interpreter: Interpreter) { + this.interpreter = interpreter + } + + getNodes(ast: Command[], filter: (x: Command) => boolean): Command[] { + return iterate(ast, [], filter) + } + + private validateVariable(ast: Command[], ctx: Context, info: AnalyzeInfo[]) { + const variables = new Set(['args', '$']) + + // check normal assignment + const assignNodes = this.getNodes(ast, (x) => x.type === 'Assign') + const assignTaskNodes = this.getNodes(ast, (x) => x.type === 'AssignTask') + const nodes = [...assignNodes, ...assignTaskNodes] + for (const node of nodes) { + const key = this.interpreter.evaluate(ctx, (node as any).symbol, false) + variables.add(key.value) + } + + // assignment in foreach loop + const assignForeachNodes = this.getNodes(ast, (x) => x.type === 'LoopFor') + for (const node of assignForeachNodes) { + const item = this.interpreter.evaluate(ctx, (node as any).item, false) + variables.add(item) + + if ((node as any).index) { + const index = this.interpreter.evaluate(ctx, (node as any).index, false) + variables.add(index) + } + } + + // assignment of args through metadata + const assignInlineArgs = this.getNodes(ast, (x) => x.type === 'Metadata' && (x as any).key === 'Args').map((x: any) => + x.value.value.map((y) => ctx.vm.evaluate(ctx, y, true)) + ) + if (assignInlineArgs && assignInlineArgs.length > 0) assignInlineArgs[0].forEach((x) => variables.add(x.name)) + + // assignment of args through taskdef + const taskDefs = this.getNodes(ast, (x) => x.type === 'TaskDef') + for (const taskdef of taskDefs) { + const args = (taskdef as any).args + if (args.length === 0) continue + const argNames = args.map((x) => ctx.vm.evaluate(ctx, x, true)).map((x) => (isString(x) ? x : x.name)) + argNames.forEach((x) => variables.add(x)) + } + + // Check Variable + const allVariables = this.getNodes(ast, (x) => x.type === 'Symbol') + for (const variable of allVariables) { + const key = this.interpreter.evaluate(ctx, variable, false) + if (!variables.has(key.value)) { + const val = variable as CommandDebug + // console.log(variable, val) + info.push( + (Object.assign( + {}, + { + level: 'error', + message: `Usage of unknown Variable '${key.value}'` + }, + val.debug + ) as unknown) as AnalyzeInfo + ) + } + } + } + + private validateFunction(ast: Command[], ctx: Context, info: AnalyzeInfo[]) { + const functions = this.getNodes(ast, (x) => x.type === 'Function') + const existing = new Set(this.interpreter.functions.keys()) + + for (const func of functions) { + const name = AkoElement.Function.name(ctx, func) + if (!existing.has(name)) { + const debug = (func as CommandDebug).debug + info.push( + (Object.assign( + {}, + { + level: 'error', + message: `Usage of unknown Function '${name}'` + }, + debug + ) as unknown) as AnalyzeInfo + ) + } + } + } + + private validateTask(ast: Command[], ctx: Context, info: AnalyzeInfo[]) { + const taskDef = this.getNodes(ast, (x) => x.type === 'TaskDef') + const tasks = this.getNodes(ast, (x) => x.type === 'Task') + + const existing = new Set(this.interpreter.tasks.keys()) + for (const task of taskDef) { + const name = AkoElement.Task.name(ctx, task) + existing.add(name) + } + + for (const task of tasks) { + const name = AkoElement.Task.name(ctx, task) + if (!existing.has(name)) { + const debug = (task as CommandDebug).debug + info.push( + (Object.assign( + {}, + { + level: 'error', + message: `Usage of unknown Task '${name}'` + }, + debug + ) as unknown) as AnalyzeInfo + ) + } + } + } + + validate(ast: Command[]) { + const fakeCtx = {vm: this.interpreter, stack: undefined} + const info: AnalyzeInfo[] = [] + this.validateVariable(ast, fakeCtx, info) + this.validateFunction(ast, fakeCtx, info) + this.validateTask(ast, fakeCtx, info) + + return info + } +} diff --git a/src/core.ts b/src/core.ts index fc969ab..6a646c5 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,5 +1,9 @@ import {Interpreter} from './interpreter' +export interface Command { + type: string +} + export type Func = (...args: any) => any export type Task = (ctx: Context, fn: any, fnData: any, time: number) => UpdateStackResult diff --git a/src/dist/ako-cli.ts b/src/dist/ako-cli.ts index bcf848b..c9a6766 100644 --- a/src/dist/ako-cli.ts +++ b/src/dist/ako-cli.ts @@ -4,38 +4,75 @@ import fs from 'fs' import path from 'path' import {listAkoFiles} from '../helpers/folder' import akoGrammar from '../../ako_grammar.txt' +import {AnalyzeInfo, Analyzer} from '../analyzer' +import {Command} from '../core' +import chalk from 'chalk' -function loadAkoModule(vm: Interpreter, projectFolder: string) { - const packagePath = path.join(projectFolder, 'module.json') +function loadAkoModule(vm: Interpreter, projectFolder: string, info: AnalyzeInfo[]) { + const packagePath = path.join(projectFolder, 'manifest.json') if (!fs.existsSync(packagePath)) return const mod = JSON.parse(fs.readFileSync(packagePath, 'utf-8')) const namespace = mod.namespace ? mod.namespace : '' - console.log('package', mod) // load deps if (mod.deps) { for (const dep of mod.deps) { - loadAkoModule(vm, path.join(projectFolder, dep)) + loadAkoModule(vm, path.join(projectFolder, dep), info) } } // load files const files = listAkoFiles(projectFolder) + const codes: [string, Command[]][] = [] for (const file of files) { const fileId = file.replace('.ako', '') const codeTxt = fs.readFileSync(path.join(projectFolder, file), 'utf-8') const match = grammar.match(codeTxt.toString()) - if (match.failed()) throw new Error(`Syntax in file "${file}", ${match.message}`) - if (!match) throw new Error(`Syntax Error with file ${path.join(projectFolder, file)}`) - const ast = ASTBuilder(match).toAST() - const methodName = namespace ? `${namespace}.${fileId}` : fileId + if (!match || match.failed()) { + info.push({ + level: 'error', + line: -1, + file: path.join(projectFolder, file), + message: `Critical Syntax Error`, + sample: match.message + }) + } else { + const ast = ASTBuilder(match).toAST() + const methodName = namespace ? `${namespace}.${fileId}` : fileId + + if (vm.tasks.has(methodName)) { + throw new Error(`Task Name Already Used : ${methodName}`) + } - console.log(`Register method : ${methodName}`) - if (vm.tasks.has(methodName)) { - throw new Error(`Task Name Already Used : ${methodName}`) + vm.addFile(methodName, ast) + codes.push([path.join(projectFolder, file), ast]) } + } - vm.addFile(methodName, ast) + const analyzer = new Analyzer(interpreter) + for (const entry of codes) { + const errors = analyzer.validate(entry[1]) + errors.forEach((x) => (x.file = entry[0])) + info = [...info, ...errors] + } + + if (info.some((x) => x.level === 'error')) { + console.log(chalk.bold(chalk.red(`[AKO CLI] Found ${info.length} Errors in the code:`))) + for (let i = 0; i < info.length; i++) { + const num = chalk.cyanBright(`${i + 1}`) + const level = + info[i].level === 'error' ? chalk.red(`[${info[i].level.toUpperCase()}]`) : chalk.yellow(`[${info[i].level.toUpperCase()}]`) + const line = info[i].line >= 0 ? ` - line ${info[i].line}` : '' + const location = chalk.green(`(Location: ${info[i].file}${line})`) + const code = + (info[i].sample_before ? info[i].sample_before : '') + + chalk.bold(info[i].sample) + + (info[i].sample_after ? info[i].sample_after : '') + console.log(`- ${level} ${num} : ${info[i].message} ${location}`) + console.log(chalk.gray(' ... ') + code.split('\n').join('\n ') + chalk.gray(' ...')) + console.log(' ') + } + process.exit(1) } // execute entry point @@ -55,7 +92,7 @@ function loadAkoModule(vm: Interpreter, projectFolder: string) { const args = process.argv.slice(2) let argFile = args.shift() if (!argFile || !fs.existsSync(argFile)) throw new Error(`File does not exists : ${argFile}`) -if (fs.lstatSync(argFile).isDirectory()) argFile = path.join(argFile, 'module.json') +if (fs.lstatSync(argFile).isDirectory()) argFile = path.join(argFile, 'manifest.json') // Parse code to AST const {grammar, ASTBuilder} = getGrammar(akoGrammar) @@ -63,9 +100,10 @@ const interpreter = new Interpreter() // Open a project const folder = path.dirname(argFile) -const modulePath = path.join(folder, 'module.json') +const modulePath = path.join(folder, 'manifest.json') if (fs.existsSync(modulePath)) { - loadAkoModule(interpreter, folder) + const info: AnalyzeInfo[] = [] + loadAkoModule(interpreter, folder, info) } else { const codeTxt = fs.readFileSync(path.resolve(argFile)) const match = grammar.match(codeTxt.toString()) @@ -75,12 +113,11 @@ if (fs.existsSync(modulePath)) { interpreter.createStack(ast) } -let counter = 10000 -;(async () => { - while (interpreter.stacks.size > 0 && counter > 0) { - interpreter.update(16) - await new Promise((resolve) => setTimeout(resolve, 16)) - counter -= 16 - } - // console.log(vm) -})() +// update interpreter with setInterval +let last = Date.now() +const inter = setInterval(() => { + const dt = Date.now() - last + interpreter.update(dt) + last = Date.now() + if (interpreter.stacks.size <= 0) clearInterval(inter) +}, 20) diff --git a/src/dist/ako-node.ts b/src/dist/ako-node.ts index e67e354..4f04aeb 100644 --- a/src/dist/ako-node.ts +++ b/src/dist/ako-node.ts @@ -1,5 +1,6 @@ import {getGrammar} from '../semantic' import {Interpreter} from '../interpreter' +import {Analyzer} from '../analyzer' import fs from 'fs' import path from 'path' @@ -15,4 +16,4 @@ const toAst = (codeTxt: string) => { if (!match) throw new Error(`Syntax Error`) return ASTBuilder(match).toAST() } -export {toAst, Interpreter} +export {toAst, Interpreter, Analyzer} diff --git a/src/dist/ako-web.ts b/src/dist/ako-web.ts index 8414e62..879e3b9 100644 --- a/src/dist/ako-web.ts +++ b/src/dist/ako-web.ts @@ -1,6 +1,7 @@ import {getGrammar} from '../semantic' import {Interpreter} from '../interpreter' import akoGrammar from '../../ako_grammar.txt' +import {Analyzer} from '../analyzer' const {grammar, ASTBuilder} = getGrammar(akoGrammar) @@ -9,4 +10,4 @@ const toAst = (codeTxt: string) => { if (!match) throw new Error(`Syntax Error`) return ASTBuilder(match).toAST() } -export {toAst, Interpreter} +export {toAst, Interpreter, Analyzer} diff --git a/src/elements/conditional.ts b/src/elements/conditional.ts index ea8cb50..d2c4f0b 100644 --- a/src/elements/conditional.ts +++ b/src/elements/conditional.ts @@ -1,8 +1,23 @@ +import {Context, Command, UpdateStackResult} from '../core' + +export interface IfCommand extends Command { + ifCond: any + ifBlock: any + elifCond: any[] + elifBlock: any[] + elseCond: any + elseBlock: any +} + +export interface IfCommandData { + meta: {block: string} +} + export const If = { - create: (ifCond, ifBlock, elifCond, elifBlock, elseBlock) => { - return {type: 'If', ifCond, ifBlock, elifCond, elifBlock, elseBlock} + create: (ifCond, ifBlock, elifCond, elifBlock, elseBlock): IfCommand => { + return {type: 'If', ifCond, ifBlock, elifCond, elifBlock, elseBlock} as IfCommand }, - initialize: (ctx, entry, entryData) => { + initialize: (ctx: Context, entry: IfCommand, entryData: IfCommandData): void => { // if if (ctx.vm.evaluate(ctx, entry.ifCond, true)) { const block = ctx.vm.createStack(entry.ifBlock.statements, { @@ -35,7 +50,7 @@ export const If = { entryData.meta = {block: block.uid} } }, - execute: (ctx, entry, entryData, timeRemains) => { + execute: (ctx: Context, entry: IfCommand, entryData: IfCommandData, timeRemains: number): UpdateStackResult => { // initialize on the first call if (!entryData.meta) If.initialize(ctx, entry, entryData) diff --git a/src/elements/function.ts b/src/elements/function.ts index 41d8426..aaea60c 100644 --- a/src/elements/function.ts +++ b/src/elements/function.ts @@ -1,3 +1,4 @@ +import {Context} from 'node:vm' import {mapArgs} from '../helpers/args' export const Metadata = { @@ -59,6 +60,14 @@ export const Task = { create: (namespace, name, args, skip) => { return {type: 'Task', namespace, name, args, skip} }, + name: (ctx, entry) => { + let fn = ctx.vm.evaluate(ctx, entry.name) + if (entry.namespace && entry.namespace.length > 0) { + const namespace = ctx.vm.evaluate(ctx, entry.namespace[0], true) + fn = `${namespace}.${fn}` + } + return fn + }, execute: (ctx, entry, entryData, timeRemains) => { if (entry.skip) { entry.args = evalArgs(ctx, entry.args) @@ -78,11 +87,7 @@ export const Task = { } if (!entryData.meta) { - let fn = ctx.vm.evaluate(ctx, entry.name) - if (entry.namespace && entry.namespace.length > 0) { - const namespace = ctx.vm.evaluate(ctx, entry.namespace[0], true) - fn = `${namespace}.${fn}` - } + const fn = Task.name(ctx, entry) const args = evalArgs(ctx, entry.args) entryData.meta = {fn, args} } @@ -124,7 +129,7 @@ export const Function = { create: (namespace, name, args) => { return {type: 'Function', namespace, name, args} }, - evaluate: (ctx, entry) => { + name: (ctx: Context, entry) => { let fn = ctx.vm.evaluate(ctx, entry.name) if (entry.namespace) { const entries = entry.namespace.map((x) => ctx.vm.evaluate(ctx, x)) @@ -132,8 +137,12 @@ export const Function = { fn = `${entries.join(',')}.${ctx.vm.evaluate(ctx, entry.name)}` } } + return fn + }, + evaluate: (ctx: Context, entry) => { + const name = Function.name(ctx, entry) const args = entry.args.map((x) => ctx.vm.evaluate(ctx, x, true)) - return ctx.vm.callFunction(fn, ...args) + return ctx.vm.callFunction(name, ...args) } } diff --git a/src/elements/loop.ts b/src/elements/loop.ts index 676703f..ecce9c7 100644 --- a/src/elements/loop.ts +++ b/src/elements/loop.ts @@ -48,7 +48,7 @@ export const LoopWhile = { entryData.meta.block = block.uid }, next: (ctx: Context, entry: LoopWhileData, entryData: any, timeRemains: number) => { - if (timeRemains <= 0) return + // if (timeRemains <= 0) return entryData.meta.cond = ctx.vm.evaluate(ctx, entry.cond, true) if (!entryData.meta.cond) return @@ -113,7 +113,7 @@ export const LoopFor = { } }, next: (ctx: Context, entry: LoopForData, entryData: any, timeRemains: number) => { - if (timeRemains <= 0) return + // if (timeRemains <= 0) return if (entryData.meta.index >= entryData.meta.iterator.length) return const block = ctx.vm.createStack(entry.block.statements, { diff --git a/src/elements/scalar.ts b/src/elements/scalar.ts index 50aa7b3..17c82b6 100644 --- a/src/elements/scalar.ts +++ b/src/elements/scalar.ts @@ -1,12 +1,22 @@ +import {Context} from 'node:vm' import {isArray, isEmpty, isObject} from '../core' const regexpVars = /{(?[\w|$]*)}/gi -export const String = { +export interface ScalarCommand { + value: T +} + +export interface ScalarCommandEntry { + create: (value: T) => ScalarCommand + evaluate: (ctx: Context, expression: ScalarCommand, resolve?: boolean) => T +} + +export const String: ScalarCommandEntry = { create: (value) => { return {type: 'String', value} }, - evaluate: (ctx, expression) => { + evaluate: (ctx, expression): string => { if (expression.value.indexOf('{') !== -1) { let str = expression.value const matches = expression.value.matchAll(regexpVars) @@ -25,16 +35,16 @@ export const String = { } } -export const Number = { +export const Number: ScalarCommandEntry = { create: (value) => { return {type: 'Number', value} }, - evaluate: (ctx, expression) => { + evaluate: (_ctx, expression) => { return expression.value } } -export const Symbol = { +export const Symbol: ScalarCommandEntry = { create: (value) => { return {type: 'Symbol', value} }, @@ -46,9 +56,9 @@ export const Symbol = { } } -export const SymbolLast = { +export const SymbolLast: ScalarCommandEntry = { create: () => { - return {type: 'SymbolLast'} + return {type: 'SymbolLast', value: undefined} }, evaluate: (ctx) => { return ctx.vm.getData(ctx, '$') diff --git a/src/interpreter.ts b/src/interpreter.ts index 44ba808..bf302df 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -172,7 +172,7 @@ export class Interpreter { const entryData = stack.elementsData[i] if (entry.type === 'TaskDef') { AkoElement.TaskDef.execute({vm: this, stack}, entry, entryData, 0.1) - console.log('Found TaskDef', entry, entryData) + // console.log('Found TaskDef', entry, entryData) } } } diff --git a/src/semantic.ts b/src/semantic.ts index a659eed..603747f 100644 --- a/src/semantic.ts +++ b/src/semantic.ts @@ -17,80 +17,100 @@ export function getGrammar(akoGrammar: string) { } }) + const debugWrapper = (start, end, res) => { + const src = start.source.sourceString + const line = src.substring(0, start.source.startIdx).split('\n').length - 1 + const sample = src.substring(start.source.startIdx, end.source.endIdx) + const before = src.substring(start.source.startIdx - 15 > 0 ? start.source.startIdx - 15 : 0, start.source.startIdx) + const after = src.substring(end.source.endIdx, end.source.endIdx + 15) + + res.debug = { + line, + sample_before: before, + sample_after: after, + sample + } + return res + } + const ASTBuilder = semantics.addOperation('toAST', { // Operator - EqExpr_eq: (a, b, c) => AkoElement.Operator.create('==', a.toAST(), c.toAST()), - EqExpr_neq: (a, b, c) => AkoElement.Operator.create('!=', a.toAST(), c.toAST()), - EqExpr_lt: (a, b, c) => AkoElement.Operator.create('<', a.toAST(), c.toAST()), - EqExpr_lte: (a, b, c) => AkoElement.Operator.create('<=', a.toAST(), c.toAST()), - EqExpr_gt: (a, b, c) => AkoElement.Operator.create('>', a.toAST(), c.toAST()), - EqExpr_gte: (a, b, c) => AkoElement.Operator.create('>=', a.toAST(), c.toAST()), - BinExpr_and: (a, b, c) => AkoElement.Operator.create('and', a.toAST(), c.toAST()), - BinExpr_or: (a, b, c) => AkoElement.Operator.create('or', a.toAST(), c.toAST()), + EqExpr_eq: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('==', a.toAST(), c.toAST())), + EqExpr_neq: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('!=', a.toAST(), c.toAST())), + EqExpr_lt: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('<', a.toAST(), c.toAST())), + EqExpr_lte: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('<=', a.toAST(), c.toAST())), + EqExpr_gt: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('>', a.toAST(), c.toAST())), + EqExpr_gte: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('>=', a.toAST(), c.toAST())), + BinExpr_and: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('and', a.toAST(), c.toAST())), + BinExpr_or: (a, b, c) => debugWrapper(a, c, AkoElement.Operator.create('or', a.toAST(), c.toAST())), - AddExpr_plus: (a, b, c) => AkoElement.MathOp.create('+', a.toAST(), c.toAST()), - AddExpr_minus: (a, b, c) => AkoElement.MathOp.create('-', a.toAST(), c.toAST()), - MulExpr_times: (a, b, c) => AkoElement.MathOp.create('*', a.toAST(), c.toAST()), - MulExpr_divide: (a, b, c) => AkoElement.MathOp.create('/', a.toAST(), c.toAST()), - MulExpr_mod: (a, b, c) => AkoElement.MathOp.create('%', a.toAST(), c.toAST()), + AddExpr_plus: (a, b, c) => debugWrapper(a, c, AkoElement.MathOp.create('+', a.toAST(), c.toAST())), + AddExpr_minus: (a, b, c) => debugWrapper(a, c, AkoElement.MathOp.create('-', a.toAST(), c.toAST())), + MulExpr_times: (a, b, c) => debugWrapper(a, c, AkoElement.MathOp.create('*', a.toAST(), c.toAST())), + MulExpr_divide: (a, b, c) => debugWrapper(a, c, AkoElement.MathOp.create('/', a.toAST(), c.toAST())), + MulExpr_mod: (a, b, c) => debugWrapper(a, c, AkoElement.MathOp.create('%', a.toAST(), c.toAST())), // Type - Number: (a) => AkoElement.Number.create(a.calc()), - bool: (a) => AkoElement.Number.create(a.sourceString === 'true' ? 1 : 0), - sqString: (a, b, c) => AkoElement.String.create(b.sourceString), - dqString: (a, b, c) => AkoElement.String.create(b.sourceString), - emptyString: (a) => AkoElement.String.create(''), - PriExpr_paren: (_, a, __) => a.toAST(), + Number: (a) => debugWrapper(a, a, AkoElement.Number.create(a.calc())), + bool: (a) => debugWrapper(a, a, AkoElement.Number.create(a.sourceString === 'true' ? 1 : 0)), + sqString: (a, b, c) => debugWrapper(a, c, AkoElement.String.create(b.sourceString)), + dqString: (a, b, c) => debugWrapper(a, c, AkoElement.String.create(b.sourceString)), + emptyString: (a) => debugWrapper(a, a, AkoElement.String.create('')), + PriExpr_paren: (_, a, __) => debugWrapper(a, a, a.toAST()), // PriExpr_pos: (_, a) => AkoElement.Number.create(+a.calc()), - PriExpr_neg: (_, a) => AkoElement.Number.create(-a.calc()), + PriExpr_neg: (_, a) => debugWrapper(a, a, AkoElement.Number.create(-a.calc())), // Conditional If: (_, ifCond, ifBlock, __, elifCond, elifBlock, ___, elseBlock) => - AkoElement.If.create(ifCond.toAST(), ifBlock.toAST(), elifCond.toAST(), elifBlock.toAST(), elseBlock.toAST()), + debugWrapper( + ifCond, + elseBlock, + AkoElement.If.create(ifCond.toAST(), ifBlock.toAST(), elifCond.toAST(), elifBlock.toAST(), elseBlock.toAST()) + ), // List - Array: (a, b, c) => AkoElement.Array.create(b.asIteration().toAST()), - Dictionary: (a, b, c) => AkoElement.Dictionary.create(b.asIteration().toAST()), - KeyValue: (a, _, c) => AkoElement.KeyValue.create(a.toAST(), c.toAST()), + Array: (a, b, c) => debugWrapper(a, c, AkoElement.Array.create(b.asIteration().toAST())), + Dictionary: (a, b, c) => debugWrapper(a, c, AkoElement.Dictionary.create(b.asIteration().toAST())), + KeyValue: (a, _, c) => debugWrapper(a, c, AkoElement.KeyValue.create(a.toAST(), c.toAST())), // - Task: (a, b, c, d, e, f, g) => AkoElement.Task.create(b.toAST(), d.toAST(), f.toAST(), false), - SkipTask: (a, b, c, d, e, f, g) => AkoElement.Task.create(b.toAST(), d.toAST(), f.toAST(), true), - TaskDef: (a, b, c, d) => AkoElement.TaskDef.create(b.toAST(), c.toAST(), d.toAST()), - Fn: (a, b, c, d, e, f) => AkoElement.Function.create(a.toAST(), c.toAST(), e.toAST()), - Arguments: (a) => a.asIteration().toAST(), - ListOf: (a) => a.asIteration().toAST(), - Pipe: (a, b, c) => AkoElement.Pipe.create(a.toAST(), c.toAST()), - Metadata: (a, b, c) => AkoElement.Metadata.create(b.toAST(), c.toAST()), + Task: (a, b, c, d, e, f, g) => debugWrapper(a, g, AkoElement.Task.create(b.toAST(), d.toAST(), f.toAST(), false)), + SkipTask: (a, b, c, d, e, f, g) => debugWrapper(a, g, AkoElement.Task.create(b.toAST(), d.toAST(), f.toAST(), true)), + TaskDef: (a, b, c, d) => debugWrapper(a, d, AkoElement.TaskDef.create(b.toAST(), c.toAST(), d.toAST())), + Fn: (a, b, c, d, e, f) => debugWrapper(a, f, AkoElement.Function.create(a.toAST(), c.toAST(), e.toAST())), + Arguments: (a) => debugWrapper(a, a, a.asIteration().toAST()), + ListOf: (a) => debugWrapper(a, a, a.asIteration().toAST()), + Pipe: (a, b, c) => debugWrapper(a, c, AkoElement.Pipe.create(a.toAST(), c.toAST())), + Metadata: (a, b, c) => debugWrapper(a, c, AkoElement.Metadata.create(b.toAST(), c.toAST())), // Assign - AssignTask: (a, _, c) => AkoElement.AssignTask.create('=', a.toAST(), c.toAST()), - AssignLeft: (a, _, c) => AkoElement.Assign.create('=', a.toAST(), c.toAST()), - AssignAdd: (a, _, c) => AkoElement.Assign.create('+=', a.toAST(), c.toAST()), - AssignSub: (a, _, c) => AkoElement.Assign.create('-=', a.toAST(), c.toAST()), - AssignIncr: (a, _) => AkoElement.Assign.create('+=', a.toAST(), AkoElement.Number.create(1)), - AssignDecr: (a, _) => AkoElement.Assign.create('-=', a.toAST(), AkoElement.Number.create(1)), + AssignTask: (a, _, c) => debugWrapper(a, c, AkoElement.AssignTask.create('=', a.toAST(), c.toAST())), + AssignLeft: (a, _, c) => debugWrapper(a, c, AkoElement.Assign.create('=', a.toAST(), c.toAST())), + AssignAdd: (a, _, c) => debugWrapper(a, c, AkoElement.Assign.create('+=', a.toAST(), c.toAST())), + AssignSub: (a, _, c) => debugWrapper(a, c, AkoElement.Assign.create('-=', a.toAST(), c.toAST())), + AssignIncr: (a, _) => debugWrapper(a, a, AkoElement.Assign.create('+=', a.toAST(), AkoElement.Number.create(1))), + AssignDecr: (a, _) => debugWrapper(a, a, AkoElement.Assign.create('-=', a.toAST(), AkoElement.Number.create(1))), // Loop - Infinite: (a, b) => AkoElement.LoopInfinite.create(b.toAST()), - While: (a, b, c) => AkoElement.LoopWhile.create(b.toAST(), c.toAST()), - Foreach: (a, b, c, d, e, f, g) => AkoElement.LoopFor.create(b.toAST(), d.toAST(), f.toAST(), g.toAST()), - Block: (a, b, c) => AkoElement.Block.create(b.toAST()), + Infinite: (a, b) => debugWrapper(a, b, AkoElement.LoopInfinite.create(b.toAST())), + While: (a, b, c) => debugWrapper(a, c, AkoElement.LoopWhile.create(b.toAST(), c.toAST())), + Foreach: (a, b, c, d, e, f, g) => debugWrapper(a, g, AkoElement.LoopFor.create(b.toAST(), d.toAST(), f.toAST(), g.toAST())), + Block: (a, b, c) => debugWrapper(a, c, AkoElement.Block.create(b.toAST())), // Lambda: (a, b, c, d, e) => { // console.log(e.sourceString) // return AkoElement.Lambda.create(b.toAST(), e.toAST()) // }, - LambdaInline: (a, b, c, d, e) => AkoElement.Lambda.create(b.toAST(), e.toAST()), - Continue: (a) => AkoElement.Continue.create(), - Return: (a, b) => AkoElement.Return.create(b.toAST()), + LambdaInline: (a, b, c, d, e) => debugWrapper(a, e, AkoElement.Lambda.create(b.toAST(), e.toAST())), + Continue: (a) => debugWrapper(a, a, AkoElement.Continue.create()), + Return: (a, b) => debugWrapper(a, b, AkoElement.Return.create(b.toAST())), // Var - id: (a) => AkoElement.String.create(a.sourceString), - Var_single: (a) => AkoElement.Symbol.create(a.toAST()), - Var_select: (a, b, c) => AkoElement.SymbolSelect.create(a.toAST(), c.toAST()), - Var_range: (a, b, c, d, e, f) => AkoElement.SymbolRange.create(a.toAST(), c.toAST(), e.toAST()), - Var_subscript: (a, b, c, d) => AkoElement.SymbolSub.create(a.toAST(), c.toAST()), - Last: (a) => AkoElement.SymbolLast.create() + id: (a) => debugWrapper(a, a, AkoElement.String.create(a.sourceString)), + Var_single: (a) => debugWrapper(a, a, AkoElement.Symbol.create(a.toAST())), + Var_select: (a, b, c) => debugWrapper(a, c, AkoElement.SymbolSelect.create(a.toAST(), c.toAST())), + Var_range: (a, b, c, d, e, f) => debugWrapper(a, f, AkoElement.SymbolRange.create(a.toAST(), c.toAST(), e.toAST())), + Var_subscript: (a, b, c, d) => debugWrapper(a, d, AkoElement.SymbolSub.create(a.toAST(), c.toAST())), + Last: (a) => debugWrapper(a, a, AkoElement.SymbolLast.create()) }) return {grammar, semantics, ASTBuilder} diff --git a/src/std/collections/list.ts b/src/std/collections/list.ts index c716cf5..df19533 100644 --- a/src/std/collections/list.ts +++ b/src/std/collections/list.ts @@ -31,5 +31,11 @@ export const list = { }, 'List.join': (arr: number[], separator: string): string => { return arr.join(separator) + }, + 'List.range': (from: number, to?: number): number[] => { + const size = to !== undefined ? to - from : from + let res = [...Array(size ?? 0).keys()] + if (to !== undefined) res = res.map((x) => x + from) + return res } } diff --git a/src/std/math/index.ts b/src/std/math/index.ts index 90b5500..a2a3957 100644 --- a/src/std/math/index.ts +++ b/src/std/math/index.ts @@ -9,5 +9,10 @@ export const math = { 'Math.min': (...args: number[]) => Math.min(...args), 'Math.abs': (arg: number) => Math.abs(arg), 'Math.ceil': (arg: number) => Math.ceil(arg), - 'Math.floor': (arg: number) => Math.floor(arg) + 'Math.floor': (arg: number) => Math.floor(arg), + 'Math.round': (arg: number) => Math.round(arg), + + // random + 'Math.rand': (min = 0, max = 1) => Math.random() * (max - min) + min, + 'Math.randint': (min = 0, max = 1) => Math.floor(Math.random() * (max + 1 - min) + min) } diff --git a/src/std/system/sleep.ts b/src/std/system/sleep.ts index fca795c..90ab137 100644 --- a/src/std/system/sleep.ts +++ b/src/std/system/sleep.ts @@ -6,11 +6,13 @@ export default { if (!entryData.elapsed) { const args = getArgs(ctx, ['duration'], entryData.meta.args) entryData.elapsed = 0 - entryData.duration = args.duration + entryData.duration = args.duration || 0 } entryData.elapsed += timeRemains - if (entryData.elapsed >= entryData.duration) return {timeRemains: entryData.elapsed - entryData.duration, done: true} + if (entryData.elapsed >= entryData.duration) { + return {timeRemains: entryData.elapsed - entryData.duration, done: true} + } return {timeRemains: 0, done: false} }, waitTasks: (ctx, entry, entryData, timeRemains) => { diff --git a/tests/interpreter/analyzer.test.ts b/tests/interpreter/analyzer.test.ts new file mode 100644 index 0000000..ea37962 --- /dev/null +++ b/tests/interpreter/analyzer.test.ts @@ -0,0 +1,106 @@ +import assert from 'assert' +import {toAst, Interpreter, Analyzer} from '../../src/dist/ako-node' + +describe('Analyzer', function () { + it('Check', () => { + const code = toAst(` +a = 1 +c = 1 +d = Math.max(4, a) +for e in [1,2,3,4] { + d = @sleep(1, 2) +} +`) + + const interpreter = new Interpreter() + const analyzer = new Analyzer(interpreter) + const info = analyzer.validate(code) + console.log(info) + + assert.strictEqual(true, true) + }) + + it('Find unknown variable', () => { + const code = toAst(` +a = 1 +c = 1 +d = Math.max(4, i) +for e in [1,2,3,4] { + d = @sleep(1, 2) +} +`) + + const interpreter = new Interpreter() + const analyzer = new Analyzer(interpreter) + const info = analyzer.validate(code) + + assert.strictEqual(info.length, 1) + assert.strictEqual(info[0].message, "Usage of unknown Variable 'i'") + }) + + it('Find unknown function', () => { + const code = toAst(` +a = 1 +c = 1 +d = Math.mox(4, a) +for e in [1,2,3,4] { + d = @sleep(1, 2) +} +`) + + const interpreter = new Interpreter() + const analyzer = new Analyzer(interpreter) + const info = analyzer.validate(code) + + assert.strictEqual(info.length, 1) + assert.strictEqual(info[0].message, "Usage of unknown Function 'Math.mox'") + }) + + it('Find unknown task', () => { + const code = toAst(` +a = 1 +c = 1 +d = Math.max(4, a) +for e in [1,2,3,4] { + d = @slaep(1, 2) +} +`) + + const interpreter = new Interpreter() + const analyzer = new Analyzer(interpreter) + const info = analyzer.validate(code) + + assert.strictEqual(info.length, 1) + assert.strictEqual(info[0].message, "Usage of unknown Task 'slaep'") + }) + + it('Find task args', () => { + const code = toAst(` +task stuff ["val"] { + return val +} + +task stuff2 ["val"] { + return val2 +} + +task stuff3 { + ## Args [ + { name = "val3", type = "int", default = 1, description = "" } + ] + return val3 +} + +@stuff() +@stuff2() +@stuff3() +`) + + const interpreter = new Interpreter() + const analyzer = new Analyzer(interpreter) + const info = analyzer.validate(code) + + assert.strictEqual(info.length, 1) + assert.strictEqual(info[0].message, "Usage of unknown Variable 'val2'") + }) +}) diff --git a/tests/std/list.test.ts b/tests/std/list.test.ts index 236a076..0d35cbd 100644 --- a/tests/std/list.test.ts +++ b/tests/std/list.test.ts @@ -71,4 +71,15 @@ b = List.contains(list, 6) assert.strictEqual((stack.data as any)['a'], 1) assert.strictEqual((stack.data as any)['b'], 0) }) + + it('Range', () => { + const {stack} = runCode(` +list = List.range() +list2 = List.range(5) +list3 = List.range(10, 20) + `) + assert.deepStrictEqual((stack.data as any)['list'], []) + assert.deepStrictEqual((stack.data as any)['list2'], [0, 1, 2, 3, 4]) + assert.deepStrictEqual((stack.data as any)['list3'], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + }) }) diff --git a/tests/std/math.test.ts b/tests/std/math.test.ts index 6fc4b97..3276f3d 100644 --- a/tests/std/math.test.ts +++ b/tests/std/math.test.ts @@ -27,4 +27,25 @@ b = List.map([0, 45, 90, 180], (val) => Angle.toDeg(Angle.toRad(val))) assert.deepStrictEqual((stack1.data as any)['a'], [0, Math.PI / 4, Math.PI / 2, Math.PI]) assert.deepStrictEqual((stack1.data as any)['b'], [0, 45, 90, 180]) }) + + it('Random Functions', () => { + const {stack: stack1} = runCode(` + listRand = [] + listRandInt = [] + for num in List.range(0, 100) { + listRand = List.append(listRand, Math.rand()) + listRandInt = List.append(listRandInt, Math.randint(1, 10)) + } + a = Math.rand(-0.5, 0.5) + b = Math.randint(-10, 10) + `) + assert.strictEqual((stack1.data as any)['listRand'].length, 100) + assert.strictEqual(Math.min(...(stack1.data as any)['listRand']) > 0, true) + assert.strictEqual(Math.max(...(stack1.data as any)['listRand']) < 1, true) + + assert.strictEqual((stack1.data as any)['listRandInt'].length, 100) + assert.strictEqual(Math.min(...(stack1.data as any)['listRandInt']) >= 1, true) + assert.strictEqual(Math.max(...(stack1.data as any)['listRandInt']) <= 10, true) + assert.strictEqual(Number.isInteger((stack1.data as any)['listRandInt'][0]), true) + }) }) diff --git a/tsconfig.json b/tsconfig.json index 1f86de0..2299cff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ "node" ], "lib": [ - "ES6", + "ES2020", "DOM" ] }, From 0aa80f3761315005d4a3bf51177e65008f9a12cb Mon Sep 17 00:00:00 2001 From: Kefniark Date: Wed, 12 May 2021 19:14:12 +0900 Subject: [PATCH 2/3] Few fixes --- src/analyzer.ts | 12 ++++++------ src/dist/ako-cli.ts | 2 +- src/std/collections/list.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/analyzer.ts b/src/analyzer.ts index cc702a6..36233ae 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -86,14 +86,14 @@ export class Analyzer { const val = variable as CommandDebug // console.log(variable, val) info.push( - (Object.assign( + Object.assign( {}, { level: 'error', message: `Usage of unknown Variable '${key.value}'` }, val.debug - ) as unknown) as AnalyzeInfo + ) as unknown as AnalyzeInfo ) } } @@ -108,14 +108,14 @@ export class Analyzer { if (!existing.has(name)) { const debug = (func as CommandDebug).debug info.push( - (Object.assign( + Object.assign( {}, { level: 'error', message: `Usage of unknown Function '${name}'` }, debug - ) as unknown) as AnalyzeInfo + ) as unknown as AnalyzeInfo ) } } @@ -136,14 +136,14 @@ export class Analyzer { if (!existing.has(name)) { const debug = (task as CommandDebug).debug info.push( - (Object.assign( + Object.assign( {}, { level: 'error', message: `Usage of unknown Task '${name}'` }, debug - ) as unknown) as AnalyzeInfo + ) as unknown as AnalyzeInfo ) } } diff --git a/src/dist/ako-cli.ts b/src/dist/ako-cli.ts index c9a6766..56a654b 100644 --- a/src/dist/ako-cli.ts +++ b/src/dist/ako-cli.ts @@ -62,7 +62,7 @@ function loadAkoModule(vm: Interpreter, projectFolder: string, info: AnalyzeInfo const num = chalk.cyanBright(`${i + 1}`) const level = info[i].level === 'error' ? chalk.red(`[${info[i].level.toUpperCase()}]`) : chalk.yellow(`[${info[i].level.toUpperCase()}]`) - const line = info[i].line >= 0 ? ` - line ${info[i].line}` : '' + const line = info[i].line >= 0 ? ` - line ${info[i].line + 1}` : '' const location = chalk.green(`(Location: ${info[i].file}${line})`) const code = (info[i].sample_before ? info[i].sample_before : '') + diff --git a/src/std/collections/list.ts b/src/std/collections/list.ts index df19533..1139192 100644 --- a/src/std/collections/list.ts +++ b/src/std/collections/list.ts @@ -34,7 +34,7 @@ export const list = { }, 'List.range': (from: number, to?: number): number[] => { const size = to !== undefined ? to - from : from - let res = [...Array(size ?? 0).keys()] + let res = [...Array(size >= 0 ? size : 0).keys()] if (to !== undefined) res = res.map((x) => x + from) return res } From 69070668556dabca961ab152688526ca9a80cba6 Mon Sep 17 00:00:00 2001 From: Kefniark Date: Wed, 12 May 2021 19:18:54 +0900 Subject: [PATCH 3/3] Add small unit test --- tests/std/math.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/std/math.test.ts b/tests/std/math.test.ts index 3276f3d..bf150c6 100644 --- a/tests/std/math.test.ts +++ b/tests/std/math.test.ts @@ -10,6 +10,7 @@ c = Math.min(1,2,12,2) d = List.map([1,-2,3], (val) => Math.abs(val)) e = List.map([1.2,-2.6,3.6], (val) => Math.ceil(val)) f = List.map([1.2,-2.6,3.6], (val) => Math.floor(val)) +g = List.map([1.2,-2.6,3.6], (val) => Math.round(val)) `) assert.strictEqual((stack1.data as any)['a'], Math.PI) assert.strictEqual((stack1.data as any)['b'], 12) @@ -17,6 +18,7 @@ f = List.map([1.2,-2.6,3.6], (val) => Math.floor(val)) assert.deepStrictEqual((stack1.data as any)['d'], [1, 2, 3]) assert.deepStrictEqual((stack1.data as any)['e'], [2, -2, 4]) assert.deepStrictEqual((stack1.data as any)['f'], [1, -3, 3]) + assert.deepStrictEqual((stack1.data as any)['g'], [1, -3, 4]) }) it('Degree Functions', () => { @@ -38,6 +40,7 @@ b = List.map([0, 45, 90, 180], (val) => Angle.toDeg(Angle.toRad(val))) } a = Math.rand(-0.5, 0.5) b = Math.randint(-10, 10) + c = Math.randint() `) assert.strictEqual((stack1.data as any)['listRand'].length, 100) assert.strictEqual(Math.min(...(stack1.data as any)['listRand']) > 0, true)