Skip to content

Commit

Permalink
Assert and unwrap utilities (#4437)
Browse files Browse the repository at this point in the history
* Add assert and unwrap utilities

* Playing around with diagnostics

* lib/assert diagnostic implementation

* Remove temporary testing stuff

* Reset package-lock.json to before I messed with it

* Further refinements and integration

* Added licence and removed an obsolete eslint directive
  • Loading branch information
jeremy-rifkin authored and mattgodbolt committed Jan 24, 2023
1 parent 93f112c commit ae6a840
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.yml
Expand Up @@ -74,6 +74,9 @@ rules:
no-useless-escape: error
no-useless-rename: error
no-useless-return: error
no-empty:
- error
- allowEmptyCatch: true
quote-props:
- error
- as-needed
Expand Down
3 changes: 3 additions & 0 deletions app.js
Expand Up @@ -67,6 +67,9 @@ import {loadSponsorsFromString} from './lib/sponsors';
import {getStorageTypeByKey} from './lib/storage';
import * as utils from './lib/utils';

// Used by assert.ts
global.ce_base_directory = __dirname; // eslint-disable-line unicorn/prefer-module

// Parse arguments from command line 'node ./app.js args...'
const opts = nopt({
env: [String, Array],
Expand Down
98 changes: 98 additions & 0 deletions lib/assert.ts
@@ -0,0 +1,98 @@
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import * as fs from 'fs';
import path from 'path';

import stacktrace from './stacktrace';

function check_path(parent: string, directory: string) {
// https://stackoverflow.com/a/45242825/15675011
const relative = path.relative(parent, directory);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
return relative;
} else {
return false;
}
}

function get_diagnostic() {
const e = new Error(); // eslint-disable-line unicorn/error-message
const trace = stacktrace.parse(e);
if (trace.length >= 4) {
const invoker_frame = trace[3];
if (invoker_frame.fileName && invoker_frame.lineNumber) {
// Just out of an abundance of caution...
const relative = check_path(global.ce_base_directory, invoker_frame.fileName);
if (relative) {
try {
const file = fs.readFileSync(invoker_frame.fileName, 'utf-8');
const lines = file.split('\n');
return {
file: relative,
line: invoker_frame.lineNumber,
src: lines[invoker_frame.lineNumber - 1].trim(),
};
} catch (e: any) {}
}
}
}
}

function fail(fail_message: string, user_message: string | undefined, args: any[]): never {
// Assertions will look like:
// Assertion failed
// Assertion failed: Foobar
// Assertion failed: Foobar, [{"foo": "bar"}]
// Assertion failed: Foobar, [{"foo": "bar"}], at `assert(x.foo.length < 2, "Foobar", x)`
let assert_line = fail_message;
if (user_message) {
assert_line += `: ${user_message}`;
}
if (args.length > 0) {
try {
assert_line += ', ' + JSON.stringify(args);
} catch (e) {}
}

const diagnostic = get_diagnostic();
if (diagnostic) {
throw new Error(assert_line + `, at ${diagnostic.file}:${diagnostic.line} \`${diagnostic.src}\``);
} else {
throw new Error(assert_line);
}
}

export function assert<C>(c: C, message?: string, ...extra_info: any[]): asserts c {
if (!c) {
fail('Assertion failed', message, extra_info);
}
}

export function unwrap<T>(x: T | undefined | null, message?: string, ...extra_info: any[]): T {
if (x === undefined || x === null) {
fail('Unwrap failed', message, extra_info);
}
return x;
}
3 changes: 2 additions & 1 deletion lib/compilers/carbon.ts
Expand Up @@ -27,6 +27,7 @@ import {CompilationResult} from '../../types/compilation/compilation.interfaces'
import {CompilerInfo} from '../../types/compiler.interfaces';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces';
import {ResultLine} from '../../types/resultline/resultline.interfaces';
import {unwrap} from '../assert';
import {BaseCompiler} from '../base-compiler';

import {BaseParser} from './argument-parsers';
Expand Down Expand Up @@ -69,7 +70,7 @@ export class CarbonCompiler extends BaseCompiler {

lastLine(lines?: ResultLine[]): string {
if (!lines || lines.length === 0) return '';
return (lines.at(-1) as ResultLine).text;
return unwrap(lines.at(-1)).text;
}

override postCompilationPreCacheHook(result: CompilationResult): CompilationResult {
Expand Down
50 changes: 10 additions & 40 deletions lib/parsers/llvm-pass-dump-parser.ts
Expand Up @@ -31,27 +31,14 @@ import {
} from '../../types/compilation/llvm-opt-pipeline-output.interfaces';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces';
import {ResultLine} from '../../types/resultline/resultline.interfaces';
import {assert} from '../assert';

// Note(jeremy-rifkin):
// For now this filters out a bunch of metadata we aren't interested in
// Maybe at a later date we'll want to allow user-controllable filters
// It'd be helpful to better display annotations about branch weights
// and parse debug info too at some point.

// TODO(jeremy-rifkin): Doe we already have an assert utility
function assert(condition: boolean, message?: string, ...args: any[]): asserts condition {
if (!condition) {
const stack = new Error('Assertion Error').stack;
throw (
(message
? `Assertion error in llvm-print-after-all-parser: ${message}`
: `Assertion error in llvm-print-after-all-parser`) +
(args.length > 0 ? `\n${JSON.stringify(args)}\n` : '') +
`\n${stack}`
);
}
}

// Just a sanity check
function passesMatch(before: string, after: string) {
assert(before.startsWith('IR Dump Before '));
Expand Down Expand Up @@ -175,9 +162,7 @@ export class LlvmPassDumpParser {
};
lastWasBlank = true; // skip leading newlines after the header
} else {
if (pass === null) {
throw 'Internal error during breakdownOutput (1)';
}
assert(pass);
if (line.text.trim() === '') {
if (!lastWasBlank) {
pass.lines.push(line);
Expand Down Expand Up @@ -216,9 +201,7 @@ export class LlvmPassDumpParser {
// function define line
if (irFnMatch || machineFnMatch) {
// if the last function has not been closed...
if (func !== null) {
throw 'Internal error during breakdownPass (1)';
}
assert(func === null);
func = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: (irFnMatch || machineFnMatch)![1],
Expand All @@ -236,19 +219,13 @@ export class LlvmPassDumpParser {
// close function
if (this.functionEnd.test(line.text.trim())) {
// if not currently in a function
if (func === null) {
throw 'Internal error during breakdownPass (2)';
}
assert(func);
const {name, lines} = func;
lines.push(line); // include the }
// loop dumps can't be terminated with }
if (name === '<loop>') {
throw 'Internal error during breakdownPass (3)';
}
assert(name !== '<loop>');
// somehow dumped twice?
if (name in pass.functions) {
throw 'Internal error during breakdownPass (4)';
}
assert(!(name in pass.functions));
pass.functions[name] = lines;
func = null;
} else {
Expand All @@ -269,17 +246,10 @@ export class LlvmPassDumpParser {
}
// unterminated function, either a loop dump or an error
if (func !== null) {
if (func.name === '<loop>') {
// loop dumps must be alone
if (Object.entries(pass.functions).length > 0) {
//console.dir(dump, { depth: 5, maxArrayLength: 100000 });
//console.log(pass.functions);
throw 'Internal error during breakdownPass (5)';
}
pass.functions[func.name] = func.lines;
} else {
throw 'Internal error during breakdownPass (6)';
}
assert(func.name === '<loop>');
// loop dumps must be alone
assert(Object.entries(pass.functions).length === 0);
pass.functions[func.name] = func.lines;
}
return pass;
}
Expand Down

0 comments on commit ae6a840

Please sign in to comment.