Skip to content

Commit

Permalink
Better ID in Rep (#48)
Browse files Browse the repository at this point in the history
More detailed failure reporting

* Better ID in Rep
* Autofix: TypeScript imports

[atomist:generated] [atomist:autofix=typescript_imports]
* lint
* Move delimited literal to someplace useful
* Remove some stringifying for test

and move the dep to devDeps, as it's only used in tests
* this conversion into tree nodes is hard
* This is effed up because it needs ArrayPatternMatch
* Delimited matcher
* Please squash later
* Important: fix isPatternMatch
  • Loading branch information
jessitron authored Jan 30, 2019
1 parent 092fb5d commit ed74051
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 30 deletions.
27 changes: 23 additions & 4 deletions lib/MatchPrefixResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,32 @@ export interface MatchPrefixResult {
*/
readonly $matcherId: string;

/**
* The matched string, if any
*/
readonly $matched: string;

}

export class MatchFailureReport implements MatchPrefixResult {

public static from(params: {
$matcherId: string,
$offset: number,
$matched?: string,
cause?: string,
children?: MatchPrefixResult[],
}): MatchFailureReport {
const { $matcherId, $offset, $matched, cause, children } = params;
return new MatchFailureReport($matcherId, $offset, $matched || "", cause,
(children as MatchFailureReport[]));
}

public constructor(public readonly $matcherId: string,
public readonly $offset: number,
capturedStructure?: {},
private readonly cause?: string | MatchFailureReport) {
public readonly $matched: string,
public readonly cause?: string,
public readonly children?: MatchFailureReport[]) {
}

get description(): string {
Expand All @@ -38,7 +56,7 @@ export class MatchFailureReport implements MatchPrefixResult {
export class SuccessfulMatch implements MatchPrefixResult {

public constructor(public readonly match: PatternMatch,
public readonly capturedStructure?: {} ) {
public readonly capturedStructure?: {}) {
if (match === undefined) {
throw new Error("You can't be successful with an undefined match");
}
Expand All @@ -49,7 +67,8 @@ export class SuccessfulMatch implements MatchPrefixResult {
get $matcherId() { return this.match.$matcherId; }

get $matched() {
return this.match.$matched; } // convenience
return this.match.$matched;
} // convenience

get $value() { return this.match.$value; } // convenience
}
Expand Down
38 changes: 30 additions & 8 deletions lib/Ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,18 @@ export class Alt implements MatchingLogic {

public matchPrefix(is: InputState, thisMatchContext: {}, parseContext: {}): MatchPrefixResult {
if (is.exhausted()) {
return new MatchFailureReport(this.$id, is.offset, {});
return new MatchFailureReport(this.$id, is.offset, "");
}

const failedMatches: MatchPrefixResult[] = [];
for (const matcher of this.matchers) {
const m = matcher.matchPrefix(is, thisMatchContext, parseContext);
if (isSuccessfulMatch(m)) {
return m;
}
failedMatches.push(m);
}
return new MatchFailureReport(this.$id, is.offset, {});
return MatchFailureReport.from({ $matcherId: this.$id, $offset: is.offset, children: failedMatches });
}
}

Expand Down Expand Up @@ -118,12 +120,32 @@ export function when(
}

function conditionalMatch(is: InputState, thisMatchContext: {}, parseContext: {}): MatchPrefixResult {
const result = inputStateTest(is) ?
matcher.matchPrefix(is, thisMatchContext, parseContext) :
undefined;
return (result && isSuccessfulMatch(result) && matchTest(result.match)) ?
result :
new MatchFailureReport(conditionalMatcher.$id, is.offset, context);
if (!inputStateTest(is)) {
return MatchFailureReport.from({
$matcherId: conditionalMatcher.$id,
$offset: is.offset,
cause: "Input state test returned false",
});
}
const result = matcher.matchPrefix(is, thisMatchContext, parseContext);
if (!isSuccessfulMatch(result)) {
return MatchFailureReport.from({
$matcherId: conditionalMatcher.$id,
$offset: is.offset,
children: [result],
cause: (result as MatchFailureReport).description,
});
}
if (!matchTest(result.match)) {
return MatchFailureReport.from({
$matcherId: conditionalMatcher.$id,
$offset: is.offset,
$matched: result.$matched,
children: [result],
cause: "Match test returned false",
});
}
return result;
}

conditionalMatcher.matchPrefix = conditionalMatch;
Expand Down
2 changes: 1 addition & 1 deletion lib/PatternMatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export abstract class PatternMatch {
}

export function isPatternMatch(mpr: PatternMatch | DismatchReport): mpr is PatternMatch {
return mpr != null && mpr !== undefined && (mpr as PatternMatch).$matched !== undefined;
return mpr != null && mpr !== undefined && (mpr as PatternMatch).$value !== undefined;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/Primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class Literal implements MatchingLogic {
public matchPrefix(is: InputState): MatchPrefixResult {
const peek = is.peek(this.literal.length);
return (peek === this.literal) ?
matchPrefixSuccess(new TerminalPatternMatch(this.$id, this.literal, is.offset, this.literal) ) :
new MatchFailureReport(this.$id, is.offset, {},
matchPrefixSuccess(new TerminalPatternMatch(this.$id, this.literal, is.offset, this.literal)) :
new MatchFailureReport(this.$id, is.offset, "", // It would be more fun to show the common portion
`Did not match literal [${this.literal}]: saw [${peek}]`);
}

Expand Down Expand Up @@ -96,7 +96,7 @@ export abstract class AbstractRegex implements MatchingLogic {
is.offset,
this.toValue(matched)));
} else {
return new MatchFailureReport(this.$id, is.offset, {},
return new MatchFailureReport(this.$id, is.offset, "",
`Did not match regex /${this.regex.source}/ in [${lookAt}]`);
}
}
Expand Down
14 changes: 11 additions & 3 deletions lib/Rep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class Repetition implements MatchingLogic, WhiteSpaceHandler {
}

get $id() {
return `Rep[${this.matcher}:min=${this.min},sep=[${this.sep}]`;
return `Rep[${this.matcher.$id}:min=${this.min},sep=[${this.sep}]`;
}

public consumeWhiteSpace(consumeWhiteSpaceBetweenTokens: boolean): this {
Expand All @@ -84,19 +84,21 @@ export class Repetition implements MatchingLogic, WhiteSpaceHandler {
let currentInputState = is;
const matches: PatternMatch[] = [];
let matched = "";
const allResults: MatchPrefixResult[] = [];
while (!currentInputState.exhausted()) {
const eat = readyToMatch(currentInputState, this.$consumeWhiteSpaceBetweenTokens);
currentInputState = eat.state;
matched += eat.skipped;

const result = this.matcher.matchPrefix(currentInputState, thisMatchContext, parseContext);
allResults.push(result);
if (!isSuccessfulMatch(result)) {
break;
} else {
const match = result.match;
if (match.$matched === "") {
throw new Error(`Matcher with id ${this.matcher.$id} within rep matched the empty string.\n` +
`I do not think this grammar means what you think it means`);
`I do not think this grammar means what you think it means`);
}
currentInputState = currentInputState.consume(match.$matched, `Rep matched [${match.$matched}]`);
matches.push(match);
Expand All @@ -108,6 +110,7 @@ export class Repetition implements MatchingLogic, WhiteSpaceHandler {
currentInputState = eaten.state;
matched += eaten.skipped;
const sepMatchResult = this.sepMatcher.matchPrefix(currentInputState, thisMatchContext, parseContext);
allResults.push(sepMatchResult);
if (isSuccessfulMatch(sepMatchResult)) {
const sepMatch = sepMatchResult.match;
currentInputState = currentInputState.consume(sepMatch.$matched, `Rep separator [${sepMatch.$matched}]`);
Expand All @@ -129,7 +132,12 @@ export class Repetition implements MatchingLogic, WhiteSpaceHandler {
matched,
is.offset,
values)) :
new MatchFailureReport(this.$id, is.offset, {});
MatchFailureReport.from({
$matcherId: this.$id,
$offset: is.offset,
$matched: matched,
children: allResults,
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/internal/Break.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class Break implements MatchingLogic {
// But we can't match the bad match if it's defined
if (this.badMatcher) {
if (isSuccessfulMatch(this.badMatcher.matchPrefix(currentIs, thisMatchContext, parseContext))) {
return new MatchFailureReport(this.$id, is.offset);
return new MatchFailureReport(this.$id, is.offset, matched);
}
}
matched += currentIs.peek(1);
Expand Down
13 changes: 10 additions & 3 deletions lib/matchers/Concat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,15 @@ export class Concat implements Concatenation, LazyMatchingLogic, WhiteSpaceHandl
const matches: PatternMatch[] = [];
let currentInputState = initialInputState;
let matched = "";
const allReportResults: MatchPrefixResult[] = [];
for (const step of this.matchSteps) {
if (isMatcher(step)) {
const eat = readyToMatch(currentInputState, this.$consumeWhiteSpaceBetweenTokens);
currentInputState = eat.state;
matched += eat.skipped;

const reportResult = step.matchPrefix(currentInputState, thisMatchContext, parseContext);
allReportResults.push(reportResult);
if (isSuccessfulMatch(reportResult)) {
const report = reportResult.match;
matches.push(report);
Expand All @@ -193,16 +195,21 @@ export class Concat implements Concatenation, LazyMatchingLogic, WhiteSpaceHandl
bindingTarget[step.$id] = report.$value;
}
} else {
return new MatchFailureReport(this.$id, initialInputState.offset, bindingTarget,
`Failed at step '${step.name}' due to ${(reportResult as any).description}`);
return MatchFailureReport.from({
$matcherId: this.$id,
$offset: initialInputState.offset,
$matched: matched,
cause: `Failed at step '${step.name}' due to ${(reportResult as any).description}`,
children: allReportResults,
});
}
} else {
// It's a function taking the contexts.
// See if we should stop matching.
if (isMatchVeto(step)) {
// tslint:disable-next-line:no-boolean-literal-compare
if (step.veto(bindingTarget, thisMatchContext, parseContext) === false) {
return new MatchFailureReport(this.$id, initialInputState.offset, bindingTarget,
return new MatchFailureReport(this.$id, initialInputState.offset, matched,
`Match vetoed by ${step.$id}`);
}
} else {
Expand Down
100 changes: 100 additions & 0 deletions lib/matchers/lang/cfamily/DelimitedLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { InputState } from "../../../InputState";
import { MatchingLogic } from "../../../Matchers";
import {
MatchFailureReport,
MatchPrefixResult,
matchPrefixSuccess,
} from "../../../MatchPrefixResult";
import { TerminalPatternMatch } from "../../../PatternMatch";
import {
LangState,
LangStateMachine,
} from "../LangStateMachine";
import {
EscapeNextCharacter,
Normal,
} from "./States";

// TODO: pass in an inner matcher, support nesting
export class DelimitedLiteral implements MatchingLogic {
public readonly $id = `${this.delimiter} ... ${this.delimiter}`;

constructor(public readonly delimiter: string,
public readonly escapeChar: string = "\\",
) {
if (delimiter.length !== 1) {
throw new Error("That is not gonna work. Delimiters are 1 char");
}
if (escapeChar.length !== 1) {
throw new Error("That is not gonna work. escapeChar must be 1 char");
}
}
public matchPrefix(is: InputState, thisMatchContext: {}, parseContext: {}):
MatchPrefixResult {
const delimiter = this.delimiter;
const initialOffset = is.offset;
let currentIs = is; // is this needed? seems likely.
if (is.peek(1) !== delimiter) {
return MatchFailureReport.from({
$matcherId: this.$id,
$offset: initialOffset,
cause: `No opening ${delimiter}; saw ${is.peek(1)} instead`,
});
}
currentIs = currentIs.consume(delimiter, "Opening delimiter");
let matched = delimiter;
const sm = new DelimiterWithEscapeChar(delimiter, this.escapeChar);
while (sm.state !== Done) {
const next = currentIs.peek(1);
if (next.length === 0) {
// out of input
return MatchFailureReport.from({
$matcherId: this.$id,
$offset: initialOffset,
cause: `End of input before the closing ${delimiter}`,
});
}
sm.consume(next);
matched += next;
currentIs = currentIs.consume(next, `Looking for a closing ${delimiter}`);
}

return matchPrefixSuccess(new TerminalPatternMatch(
this.$id, matched, is.offset, matched));
}
}

const Done = new LangState("DONE", false, false);
class DelimiterWithEscapeChar extends LangStateMachine {

constructor(public readonly endChar: string,
public readonly escapeChar: string,
state: LangState = Normal) {
super(state);
}

public clone(): DelimiterWithEscapeChar {
return new DelimiterWithEscapeChar(this.endChar, this.escapeChar, this.state);
}

public consume(ch: string): void {
switch (this.state) {
case Done:
break;
case EscapeNextCharacter:
this.state = Normal;
break;
case Normal:
switch (ch) {
case this.escapeChar:
this.state = EscapeNextCharacter;
break;
case this.endChar:
this.state = Done;
break;
default:
// no change
}
}
}
}
1 change: 1 addition & 0 deletions lib/matchers/lang/cfamily/States.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export const Normal = new LangState("normal", false, false);
export const DoubleString = new LangState("string", false, true);
export const SlashStarComment = new LangState("/*comment", true, false);
export const SlashSlashComment = new LangState("//comment", true, false);
export const EscapeNextCharacter = new LangState("\\x", false, false);
6 changes: 6 additions & 0 deletions lib/matchers/lang/cfamily/javascript/regexpLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MatchingLogic } from "../../../../Matchers";
import { DelimitedLiteral } from "../DelimitedLiteral";

export function regexLiteral(): MatchingLogic {
return new DelimitedLiteral("/");
}
4 changes: 2 additions & 2 deletions lib/matchers/snobol/Span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class Span implements MatchingLogic {
currentIs = currentIs.advance();
}
return (currentIs !== is) ?
matchPrefixSuccess(new TerminalPatternMatch(this.$id, matched, is.offset, currentIs) ) :
new MatchFailureReport(this.$id, is.offset, context);
matchPrefixSuccess(new TerminalPatternMatch(this.$id, matched, is.offset, currentIs)) :
new MatchFailureReport(this.$id, is.offset, matched);
}
}
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ed74051

Please sign in to comment.