Skip to content

Commit 4553fd7

Browse files
committed
Add support for @conflict markers for external token sets
FEATURE: `@external tokens` declarations now support `@conflict { some, tokens }` declarations to check that they are never used in a context that also allows those tokens.
1 parent 7748a6d commit 4553fd7

File tree

3 files changed

+53
-12
lines changed

3 files changed

+53
-12
lines changed

src/build.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ class Builder {
277277
.map((grp, i) => grp.buildLocalGroup(fullTable, skipInfo, i))
278278
let {tokenGroups, tokenPrec, tokenData} =
279279
time("Build token groups", () => this.tokens.buildTokenGroups(fullTable, skipInfo, localTokens.length))
280+
for (let ext of this.externalTokens) ext.checkConflicts(fullTable, skipInfo)
280281
let table = time("Finish automaton", () => finishAutomaton(fullTable))
281282
let skipState = findSkipStates(table, this.terms.tops)
282283

@@ -1586,7 +1587,7 @@ class MainTokenSet extends TokenSet {
15861587
let id = JSON.stringify(expr.value), found = this.built.find(b => b.id == id)
15871588
if (found) return found.term
15881589
}
1589-
this.b.warn(`Precedence specified for unknown token ${expr}`, expr.start)
1590+
this.b.warn(`Conflict specified for unknown token ${expr}`, expr.start)
15901591
return null
15911592
}
15921593
for (let c of this.ast?.conflicts || []) {
@@ -1895,6 +1896,36 @@ class ExternalTokenSet implements TokenizerSpec {
18951896

18961897
getToken(expr: NameExpression) { return findExtToken(this.b, this.tokens, expr) }
18971898

1899+
checkConflicts(states: readonly LRState[], skipInfo: readonly SkipInfo[]) {
1900+
let conflicting: Term[] = []
1901+
for (let id of this.ast.conflicts) {
1902+
let term = this.b.namedTerms[id.name]
1903+
if (!term) {
1904+
this.b.warn(`Unknown conflict term '${id.name}'`)
1905+
} else if (!term.terminal) {
1906+
this.b.warn(`Term '${id.name}' isn't a terminal and cannot be used in a token conflict.`)
1907+
} else if (this.tokens[id.name]) {
1908+
this.b.warn(`External token set specifying a conflict with one of its own tokens ('${id.name}')`)
1909+
} else {
1910+
conflicting.push(term)
1911+
}
1912+
}
1913+
if (conflicting.length) for (let state of states) {
1914+
let skip = skipInfo[this.b.skipRules.indexOf(state.skip)].startTokens, relevant = false, conflict: Term | null = null
1915+
for (let i = 0; i < state.actions.length + skip.length; i++) {
1916+
let term = i < state.actions.length ? state.actions[i].term : skip[i - state.actions.length]
1917+
if (term.name in this.tokens) {
1918+
relevant = true
1919+
} else if (conflicting.indexOf(term) > -1) {
1920+
conflict = term
1921+
}
1922+
}
1923+
if (relevant && conflict)
1924+
this.b.raise(`Tokens from external group used together with conflicting token '${conflict.name}'
1925+
After: ${state.set[0].trail()}`, this.ast.start)
1926+
}
1927+
}
1928+
18981929
create() {
18991930
return this.b.options.externalTokenizer!(this.ast.id.name, this.b.termTable)
19001931
}

src/node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ export class ExternalTokenDeclaration extends Node {
9797
constructor(start: number,
9898
readonly id: Identifier,
9999
readonly source: string,
100-
readonly tokens: readonly {id: Identifier, props: readonly Prop[]}[]) {
100+
readonly tokens: readonly {id: Identifier, props: readonly Prop[]}[],
101+
readonly conflicts: readonly Identifier[]) {
101102
super(start)
102103
}
103104
}

src/parse.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -484,31 +484,40 @@ function parseTokenConflict(input: Input) {
484484
return new TokenConflictDeclaration(start, a, b)
485485
}
486486

487-
function parseExternalTokenSet(input: Input) {
488-
let tokens: {id: Identifier, props: readonly Prop[]}[] = []
487+
function parseExternalTokenSet(input: Input, allowConflicts: boolean) {
488+
let tokens: {id: Identifier, props: readonly Prop[]}[] = [], conflicts: Identifier[] = []
489489
input.expect("{")
490-
while (!input.eat("}")) {
491-
if (tokens.length) input.eat(",")
492-
let id = parseIdent(input)
493-
let props = parseProps(input)
494-
tokens.push({id, props})
490+
for (let first = true; !input.eat("}"); first = false) {
491+
if (!first) input.eat(",")
492+
if (allowConflicts && input.eat("at", "conflict")) {
493+
input.expect("{")
494+
for (let f = true; !input.eat("}"); f = false) {
495+
if (!f) input.eat(",")
496+
conflicts.push(parseIdent(input))
497+
}
498+
} else {
499+
let id = parseIdent(input)
500+
let props = parseProps(input)
501+
tokens.push({id, props})
502+
}
495503
}
496-
return tokens
504+
return {tokens, conflicts}
497505
}
498506

499507
function parseExternalTokens(input: Input, start: number) {
500508
let id = parseIdent(input)
501509
input.expect("id", "from")
502510
let from = input.expect("string")
503-
return new ExternalTokenDeclaration(start, id, from, parseExternalTokenSet(input))
511+
let {tokens, conflicts} = parseExternalTokenSet(input, true)
512+
return new ExternalTokenDeclaration(start, id, from, tokens, conflicts)
504513
}
505514

506515
function parseExternalSpecialize(input: Input, type: "extend" | "specialize", start: number) {
507516
let token = parseBracedExpr(input)
508517
let id = parseIdent(input)
509518
input.expect("id", "from")
510519
let from = input.expect("string")
511-
return new ExternalSpecializeDeclaration(start, type, token, id, from, parseExternalTokenSet(input))
520+
return new ExternalSpecializeDeclaration(start, type, token, id, from, parseExternalTokenSet(input, false).tokens)
512521
}
513522

514523
function parseExternalPropSource(input: Input, start: number) {

0 commit comments

Comments
 (0)