Add a minimally working test infrastructure

This commit is contained in:
Sam Vervaeck 2020-06-07 00:21:15 +02:00
parent 3dcf91c520
commit 608728ade9
10 changed files with 802 additions and 37 deletions

137
package-lock.json generated
View file

@ -403,6 +403,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.11.tgz",
"integrity": "sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg=="
},
"@types/ora": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@types/ora/-/ora-3.2.0.tgz",
"integrity": "sha512-jll99xUKpiFbIFZSQcxm4numfsLaOWBzWNaRk3PvTSE7BPqTzzOCFmS0mQ7m8qkTfmYhuYbehTGsxkvRLPC++w==",
"requires": {
"ora": "*"
}
},
"@types/semver": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz",
@ -656,7 +664,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@ -1211,6 +1218,19 @@
}
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"requires": {
"restore-cursor": "^3.1.0"
}
},
"cli-spinners": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz",
"integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w=="
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -1221,6 +1241,11 @@
"wrap-ansi": "^6.2.0"
}
},
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -1235,7 +1260,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@ -1243,8 +1267,7 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"commander": {
"version": "2.20.3",
@ -1576,6 +1599,14 @@
"type-detect": "^4.0.0"
}
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"requires": {
"clone": "^1.0.2"
}
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -1802,8 +1833,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint-scope": {
"version": "4.0.3",
@ -2273,8 +2303,7 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-symbols": {
"version": "1.0.1",
@ -2615,6 +2644,11 @@
"is-extglob": "^2.1.1"
}
},
"is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -2798,7 +2832,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2"
},
@ -2807,7 +2840,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@ -2818,7 +2850,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@ -3055,8 +3086,7 @@
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"minimalistic-assert": {
"version": "1.0.1",
@ -3320,6 +3350,11 @@
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -3556,6 +3591,62 @@
"wrappy": "1"
}
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"ora": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz",
"integrity": "sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==",
"requires": {
"chalk": "^3.0.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.2.0",
"is-interactive": "^1.0.0",
"log-symbols": "^3.0.0",
"mute-stream": "0.0.8",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"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=="
}
}
},
"os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
@ -3992,6 +4083,15 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
@ -4134,8 +4234,7 @@
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"snapdragon": {
"version": "0.8.2",
@ -5135,6 +5234,14 @@
}
}
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"requires": {
"defaults": "^1.0.3"
}
},
"webpack": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz",

View file

@ -9,7 +9,8 @@
"scripts": {
"watch": "webpack --watch --config webpack.dev.js",
"prepare": "webpack --config webpack.dev.js",
"test": "mocha lib/test"
"test": "node build/bin/bolt-test.js",
"update-lkg": "node build/bin/bolt-test.js snapshot lkg"
},
"author": "Sam Vervaeck <vervaeck.sam@skynet.be>",
"license": "GPL-3.0",
@ -21,6 +22,7 @@
"@types/microtime": "^2.1.0",
"@types/minimist": "^1.2.0",
"@types/node": "^14.0.11",
"@types/ora": "^3.2.0",
"@types/semver": "^7.2.0",
"@types/yargs": "^15.0.5",
"chalk": "^4.0.0",
@ -30,6 +32,7 @@
"microtime": "^3.0.0",
"minimist": "^1.2.5",
"moment": "^2.26.0",
"ora": "^4.0.4",
"pegjs": "^0.11.0-master.b7b87ea",
"reflect-metadata": "^0.1.13",
"semver": "^7.3.2",

370
src/bin/bolt-test.ts Normal file
View file

@ -0,0 +1,370 @@
import "source-map-support/register"
import * as fs from "fs-extra"
import * as path from "path"
import * as crypto from "crypto"
import yargs from "yargs"
import yaml from "js-yaml"
import { sync as globSync } from "glob"
import ora from "ora"
import { Parser } from "../parser"
import { Scanner } from "../scanner"
import { SyntaxKind, Syntax } from "../ast"
import { Json, serialize, JsonObject, MapLike, upsearchSync, deepEqual } from "../util"
import { DiagnosticIndex } from "../diagnostics"
import { TextFile, TextPos, TextSpan } from "../text"
const PACKAGE_ROOT = path.dirname(upsearchSync('package.json')!);
const STORAGE_DIR = path.join(PACKAGE_ROOT, '.test-storage');
const spinner = ora(`Initializing test session ...`).start();
function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
}
return value === undefined || value === null ? [] : [ value ]
}
class Test {
public key: string;
public result?: Json;
public error: Error | null = null;
constructor(
public readonly span: TextSpan,
public readonly type: string,
public readonly text: string,
public readonly data: JsonObject,
) {
this.key = hash([text, data]);
}
}
interface LoadTestsOptions {
include: string[];
exclude: string[];
}
class TestSession {
private failCount = 0;
public key: string;
constructor(private tests: Test[] = []) {
this.key = `${Date.now().toString()}-${Math.random()}`;
}
public getAllTests() {
return this.tests[Symbol.iterator]();;
}
public scanForTests(options?: LoadTestsOptions) {
const includes = options?.include ?? ['test/**/*.md'];
const excludes = options?.exclude ?? [];
spinner.text = 'Scanning for tests [0 found]';
for (const include of includes) {
for (const filepath of globSync(include, { ignore: excludes })) {
spinner.info(`Found file ${filepath}`)
for (const test of loadTests(filepath)) {
this.tests.push(test);
spinner.text = `Scanning for tests [${this.tests.length} found]`;
}
}
}
}
public run() {
let i = 1;
//let failed = [];
for (const test of this.tests) {
spinner.text = `Running tests [${i}/${this.tests.length}]`
const runner = TEST_RUNNERS[test.type]
if (runner === undefined) {
spinner.warn(`Test runner '${test.type}' not found.`)
continue;
}
let result;
try {
test.result = runner(test);
} catch (e) {
test.error = e;
this.failCount++;
//failed.push(test);
spinner.warn(`The following test from ${path.relative(process.cwd(), test.span.file.fullPath)} failed with "${e.message}":\n\n${test.text}\n`)
}
i++;
}
if (this.failCount > 0) {
spinner.fail(`${this.failCount} tests failed.`)
}
}
public save() {
fs.mkdirpSync(path.join(STORAGE_DIR, 'snapshots', this.key))
for (const test of this.tests) {
fs.writeFileSync(path.join(STORAGE_DIR, 'snapshots', this.key, test.key), JSON.stringify(test.result), 'utf8');
}
}
public hasFailedTests() {
return this.failCount > 0;
}
}
function compare(actual: string, expected: string) {
for (const testKey of fs.readdirSync(path.join(STORAGE_DIR, 'snapshots', actual))) {
const actualTestResult = readJson(path.join(STORAGE_DIR, 'snapshots', actual, testKey))!;
const expectedTestResult = readJson(path.join(STORAGE_DIR, 'snapshots', expected, testKey));
if (!deepEqual(actualTestResult, expectedTestResult)) {
spinner.warn(`Test ${testKey} does not compare to its expected value.`)
}
}
}
function isWhiteSpace(ch: string): boolean {
return /[\n\r\t ]/.test(ch);
}
interface TestFileMetadata {
type: string;
expect: string;
}
function* loadTests(filepath: string): IterableIterator<Test> {
const file = new TextFile(filepath);
const contents = file.getText('utf8');
let i = 0;
let column = 1
let line = 1;
let atNewLine = true;
assertText('---');
let yamlStr = '';
i += 3;
while (!lookaheadEquals('---')) {
yamlStr += contents[i++];
}
i += 3;
const metadata = yaml.safeLoad(yamlStr);
while (i < contents.length) {
skipWhiteSpace();
if (atNewLine && column >= 5) {
const startPos = new TextPos(i, line, column);
const text = scanCodeBlock();
const endPos = new TextPos(i, line, column);
if (metadata['split-lines']) {
for (const line of text.split('\n')) {
if (line.trim() !== '') {
yield new Test(new TextSpan(file, startPos.clone(), endPos), metadata.type, line, metadata);
startPos.advance(line);
}
}
} else {
yield new Test(new TextSpan(file, startPos, endPos), metadata.type, text, metadata);
}
} else {
getChar();
}
}
function getChar() {
const ch = contents[i++];
if (ch === '\n') {
column = 1;
line++;
atNewLine = true;
} else {
if (!isEmpty(ch)) {
atNewLine = false;
}
column++;
}
return ch;
}
function assertText(str: string) {
for (let k = 0; k < str.length; k++) {
if (contents[i+k] !== str[k]) {
throw new Error(`Expected '${str}' but got ${contents.substr(i, i+str.length)}`)
}
}
}
function isEmpty(ch: string): boolean {
return /[\t ]/.test(ch);
}
function lookaheadEquals(str: string): boolean {
for (let k = 0; k < str.length; k++) {
if (contents[i+k] !== str[k]) {
return false;
}
}
return true;
}
function scanCodeBlock() {
let out = ''
while (i < contents.length) {
const ch = getChar();
if (ch === '\n') {
out += ch;
skipWhiteSpace();
continue;
}
if (column < 5) {
break;
}
out += ch;
}
return out;
}
function skipWhiteSpace() {
takeWhile(isWhiteSpace)
}
function takeWhile(pred: (ch: string) => boolean) {
let out = '';
while (true) {
const c0 = contents[i];
if (!pred(c0)) {
break
}
out += getChar();
}
return out;
}
}
function readJson(filename: string): Json | null {
try {
return JSON.parse(fs.readFileSync(filename, 'utf8'));
} catch (e) {
if (e.code === 'ENOENT') {
return null
}
throw e;
}
}
function hash(value: Json) {
const hasher = crypto.createHash('sha256');
hasher.update(JSON.stringify(value));
return hasher.digest('hex');
}
type TestRunner = (test: Test) => Json;
const TEST_RUNNERS: MapLike<TestRunner> = {
scan(test: Test): Json {
const diagnostics = new DiagnosticIndex;
const scanner = new Scanner(test.span.file, test.text, test.span.start);
const tokens = []
while (true) {
const token = scanner.scan();
if (token.kind === SyntaxKind.EndOfFile) {
break;
}
tokens.push(token);
}
return serialize({
diagnostics: [...diagnostics.getAllDiagnostics()],
tokens,
});
},
parse(test: Test): Json {
const kind = test.data.expect ?? 'SourceFile';
const diagnostics = new DiagnosticIndex;
const parser = new Parser();
const tokens = new Scanner(test.span.file, test.text);
let results: Syntax[];
switch (kind) {
case 'SourceFile':
results = [ parser.parseSourceFile(tokens) ];
break;
case 'SourceElements':
results = parser.parseSourceElements(tokens);
break;
default:
throw new Error(`I did not know how to parse ${kind}`)
}
return serialize({
diagnostics: [...diagnostics.getAllDiagnostics()],
results,
})
},
}
yargs
.command(['$0 [pattern..]', 'run [pattern..]'], 'Run all tests on the current version of the compiler',
yargs => yargs
.array('pattern')
.describe('pattern', 'Only run the tests matching the given pattern')
.array('include')
.describe('include', 'Files to scan for tests')
.default('include', ['test/**/*.md'])
.array('exclude')
.describe('exclude', 'Files to never scan for tests')
.default('exclude', [])
, args => {
const session = new TestSession();
session.scanForTests(args as LoadTestsOptions);
session.run();
session.save();
if (session.hasFailedTests()) {
return;
}
const expectedKey = fs.readFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), 'utf8')
compare(session.key, expectedKey)
}
)
.command(['snapshot [name]'], 'Create a new snapshot from the output of the current compiler',
yargs => yargs
.array('include')
.describe('include', 'Files to scan for tests')
.default('include', ['test/**/*.md'])
.array('exclude')
.describe('exclude', 'Files to never scan for tests')
.default('exclude', [])
, args => {
// Load and run all tests, saving the results to disk
const session = new TestSession();
session.scanForTests(args as LoadTestsOptions);
session.run();
session.save();
// Set the tests that we have run as being the new 'lkg'
fs.mkdirpSync(path.join(STORAGE_DIR, 'aliases'));
fs.writeFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), session.key, 'utf8')
}
)
.command('inspect <pattern..>', 'Inspect the ouput of a given test',
yargs => yargs
, args => {
}
)
.version()
.help()
.argv;

View file

@ -52,6 +52,7 @@ import {
createBoltExMark,
createBoltWhereKeyword,
} from "./ast"
import { outputFile } from "fs-extra";
export enum PunctType {
Paren,
@ -111,11 +112,11 @@ function isNewLine(ch: string) {
}
function isIdentStart(ch: string) {
return /[_\p{L}]/u.test(ch)
return /[_\p{ID_Start}]/u.test(ch)
}
function isIdentPart(ch: string) {
return /[_\p{L}\p{Nd}]/u.test(ch)
return /[_\p{ID_Continue}]/u.test(ch)
}
function isSymbol(ch: string) {
@ -181,6 +182,86 @@ export class Scanner {
return text;
}
private scanHexDigit(): number {
const c0 = this.peekChar();
let out;
switch (c0) {
case '0': out = 0; break;
case '1': out = 1; break;
case '2': out = 2; break;
case '3': out = 3; break;
case '4': out = 4; break;
case '5': out = 5; break;
case '6': out = 6; break;
case '7': out = 7; break;
case '8': out = 8; break;
case '9': out = 9; break;
case 'a': out = 10; break;
case 'b': out = 11; break;
case 'c': out = 12; break;
case 'd': out = 13; break;
case 'e': out = 14; break;
case 'f': out = 15; break;
case 'A': out = 10; break;
case 'B': out = 11; break;
case 'C': out = 12; break;
case 'D': out = 13; break;
case 'E': out = 14; break;
case 'F': out = 15; break;
default:
throw new ScanError(this.file, this.currPos.clone(), c0);
}
this.getChar();
return out;
}
private scanEscapeSequence(): string {
this.assertChar('\\')
const c0 = this.peekChar();
switch (c0) {
case 'a':
this.getChar();
return '\n'
case 'b':
this.getChar();
return '\b'
case 'f':
this.getChar();
return '\f'
case 'n':
this.getChar();
return '\n'
case 'r':
this.getChar();
return '\r'
case 't':
this.getChar();
return '\t'
case 'v':
this.getChar();
return '\v'
case '0':
this.getChar();
return '\0'
case 'u':
{
const d0 = this.scanHexDigit();
const d1 = this.scanHexDigit();
const d2 = this.scanHexDigit();
const d3 = this.scanHexDigit();
return String.fromCharCode(d0 * (16 ** 3) + d1 * (16 ** 2) + d2 * 16 + d3);
}
case 'x':
{
const d0 = this.scanHexDigit();
const d1 = this.scanHexDigit();
return String.fromCharCode(d0 * 16 + d1);
}
default:
throw new ScanError(this.file, this.currPos.clone(), c0);
}
}
public scan(): BoltToken {
while (true) {
@ -227,15 +308,17 @@ export class Scanner {
let text = ''
while (true) {
const c1 = this.getChar();
const c1 = this.peekChar();
if (c1 === EOF) {
throw new ScanError(this.file, this.currPos.clone(), EOF);
}
if (c1 === '"') {
this.getChar();
break;
} else if (c1 === '\\') {
this.scanEscapeSequence()
text += this.scanEscapeSequence()
} else {
this.getChar();
text += c1
}
}
@ -246,7 +329,7 @@ export class Scanner {
} else if (isDigit(c0)) {
const digits = this.takeWhile(isDigit)
const digits = this.takeWhile(isDigit);
const endPos = this.currPos.clone();
return createBoltIntegerLiteral(BigInt(digits), new TextSpan(this.file, startPos, endPos));

View file

@ -1,6 +1,7 @@
import * as path from "path"
import * as fs from "fs"
import { serializeTag, serialize } from "./util";
export class TextFile {
@ -14,11 +15,15 @@ export class TextFile {
return path.resolve(this.origPath)
}
public getText(): string {
[serializeTag]() {
return this.origPath;
}
public getText(encoding: BufferEncoding = 'utf8'): string {
if (this.cachedText !== null) {
return this.cachedText;
}
const text = fs.readFileSync(this.fullPath, 'utf8');
const text = fs.readFileSync(this.fullPath, encoding);
this.cachedText = text;
return text
}
@ -39,6 +44,26 @@ export class TextPos {
return new TextPos(this.offset, this.line, this.column)
}
[serializeTag]() {
return {
offset: this.offset,
line: this.line,
column: this.column,
}
}
public advance(str: string) {
for (const ch of str) {
if (ch === '\n') {
this.line++;
this.column = 1;
} else {
this.column++;
}
this.offset++;
}
}
}
export class TextSpan {
@ -55,5 +80,13 @@ export class TextSpan {
return new TextSpan(this.file, this.start.clone(), this.end.clone());
}
[serializeTag]() {
return {
file: serialize(this.file),
start: serialize(this.start),
end: serialize(this.end),
}
}
}

View file

@ -6,14 +6,12 @@ import * as os from "os"
import moment from "moment"
import chalk from "chalk"
import { LOG_DATETIME_FORMAT } from "./constants"
import { E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER } from "./diagnostics"
import { isPrimitive } from "util"
export function isPowerOf(x: number, n: number):boolean {
const a = Math.log(x) / Math.log(n);
return Math.pow(a, n) == x;
}
export function some<T>(iterator: Iterator<T>, pred: (value: T) => boolean): boolean {
while (true) {
const { value, done } = iterator.next();
@ -194,15 +192,31 @@ export type Newable<T> = {
new(...args: any): T;
}
export function isPrimitive(value: any): boolean {
return (typeof(value) !== 'function' && typeof(value) !== 'object') || value === null;
}
export const serializeTag = Symbol('serializer tag');
const serializableClasses = new Map<string, Newable<{ [serializeTag](): Json }>>();
export function serialize(value: any): Json {
if (isPrimitive(value)) {
if (typeof(value) === 'bigint') {
return {
type: 'bigint',
value: value.toString(),
}
} else {
return {
type: 'primitive',
value,
}
}
} else if (Array.isArray(value)) {
return {
type: 'primitive',
value,
type: 'array',
elements: value.map(serialize)
}
} else if (isObjectLike(value)) {
if (isPlainObject(value)) {
@ -219,13 +233,9 @@ export function serialize(value: any): Json {
data: value[serializeTag](),
}
} else {
console.log(value);
throw new Error(`Could not serialize ${value}: it was a non-primitive object and has no serializer tag.`)
}
} else if (Array.isArray(value)) {
return {
type: 'array',
elements: value.map(serialize)
}
} else {
throw new Error(`Could not serialize ${value}: is was not recognised as a primitive type, an object, a class instance, or an array.`)
}
@ -503,8 +513,8 @@ export function upsearchSync(filename: string, startDir = '.') {
if (fs.existsSync(filePath)) {
return filePath
}
const { root, dir } = path.parse(currDir);
if (dir === root) {
const { root, dir } = path.parse(currDir);
if (currDir === root) {
return null;
}
currDir = dir;
@ -638,3 +648,29 @@ export function format(message: string, data: MapLike<FormatArg>) {
}
export function deepEqual(a: any, b: any): boolean {
if (isPrimitive(a) && isPrimitive(b)) {
return a === b;
} else if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
} else if (isPlainObject(a) && isPlainObject(b)) {
const unmarked = new Set(Object.keys(b));
for (const key of Object.keys(a)) {
if (!deepEqual(a[key], b[key])) {
return false;
}
unmarked.delete(key);
}
return unmarked.size === 0;
}
return false;
}

View file

@ -0,0 +1,53 @@
---
type: scan
expect: BoltIdentifier
split-lines: true
---
The most simple identifiers are those made out of ASCII letters:
Foo
Bar
Baz
However, they may also contain digits as long as they do not begin with a
digit:
Var1
Var2
Var10029384
Identifiers may be as long as you want:
ThisIsALongAndValidIdentifier
ThisIsAnEvenLongButStilCompletelyValidIdentifier
Moreover, they may have arbitrary underscores (`_`) in their names.
a_valid_identifier
another__0000__valid_identfier
_1
__2
___3
They may even be nothing more than underscores:
_
__
___
All identifiers starting with a `ID_Start` character are valid identifiers,
including `Other_ID_Start`:
℘rototype
llipsis
Likewise, the following code points using `Other_ID_Continue` are also valid:
α·β
ano·teleia
And, of course, the combination of `ID_Start` and `ID_Continue`:
alfa·beta

View file

@ -0,0 +1,20 @@
---
type: scan
expect: BoltStringLiteral
split-lines: true
---
A string may hold arbirary ASCII characters, including spaces:
"Foo!"
"Once upon a time ..."
Special ASCII characters have no effect, other than that they are appended to
the contents of the string:
"S+me w3!rd @SCII ch@r$"
Some special escape sequences:
"\n\r"
"\n"

View file

@ -0,0 +1,57 @@
---
type: scan
expect: BoltIntegerLiteral
split-lines: true
---
All decimal digits are valid integers.
1
2
3
4
5
6
7
8
9
0
Any combination of decimal digits are valid integers, including integers
prefixed with an abirary amount of zeroes.
12345
99
10
01
000
0010
In binary mode, integers are read in base-2.
0b0
0b1
0b10010
0b00100
0b00000
This means the following expressions are invalid in binary mode:
0b20001
0b12345
0b00003
In octal mode, integers are read in base-8.
0o0
0o00000
0o007
0o706
0o12345
This means the following expressions are invalid in octal mode:
0o8
0o9
0o123456789

View file

@ -5,9 +5,12 @@ const path = require("path");
module.exports = {
target: 'node',
mode: 'development',
entry: './src/bin/bolt.ts',
entry: {
'bolt': './src/bin/bolt.ts',
'bolt-test': './src/bin/bolt-test.ts',
},
output: {
filename: 'bin/bolt.js',
filename: 'bin/[name].js',
path: path.resolve(__dirname, 'build'),
},
resolve: {