Skip to content

Commit 9c91c50

Browse files
committed
Support parsing set forks
1 parent ad48295 commit 9c91c50

File tree

11 files changed

+63
-12
lines changed

11 files changed

+63
-12
lines changed

new-version-ideas.org

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
#+end_src
2525
** Ideally leave room for some way to do smart grammar things
2626
Things like pluralization matching can be pretty tedious and often push me to skipping some chancelation variety. If there was some kind of way to support this in the language with a plugin that would be very helpful. It seems pretty complicated though..
27-
** TODO Support "re-using" choice blocks
27+
** DONE Support "re-using" choice blocks
2828
Sometimes I find myself wanting to reuse a choice block without copying the picked result from the original. Maybe this could be done with syntax like:
2929

3030
#+begin_src bml

src/analysis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ function deriveForkNode(choiceFork: ChoiceFork, forkIdMap: ForkIdMap): ForkNode
9595
}
9696

9797
function deriveRefNode(ref: Reference, forkIdMap: ForkIdMap): RefNode {
98+
// TODO handle re-executing refs
9899
let forkMapLookupResult = forkIdMap.get(ref.id);
99100
if (!forkMapLookupResult) {
100101
// Handle unmapped refs gracefully - this is expected

src/choiceFork.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ export class ChoiceFork {
1313
weights: WeightedChoice[];
1414
identifier: string | null;
1515
isSilent: boolean;
16+
isSet: boolean;
1617

17-
constructor(weights: WeightedChoice[], identifier: string | null, isSilent: boolean) {
18+
constructor(weights: WeightedChoice[], identifier: string | null, isSilent: boolean, isSet: boolean) {
1819
this.weights = normalizeWeights(weights);
1920
this.identifier = identifier;
2021
this.isSilent = isSilent;
22+
this.isSet = isSet;
2123
}
2224

2325
/**

src/lexer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,15 @@ export class Lexer {
9393
} else if (this.str[this.index] === '@') {
9494
tokenType = TokenType.AT;
9595
tokenString = '@';
96+
} else if (this.str[this.index] === '#') {
97+
tokenType = TokenType.HASH;
98+
tokenString = '#';
9699
} else if (this.str[this.index] === '!') {
97100
tokenType = TokenType.BANG;
98101
tokenString = '!';
102+
} else if (this.str[this.index] === '$') {
103+
tokenType = TokenType.DOLLAR;
104+
tokenString = '$';
99105
} else if (this.str[this.index] === '[') {
100106
tokenType = TokenType.OPEN_BRACKET;
101107
tokenString = '[';
@@ -152,6 +158,7 @@ export class Lexer {
152158
if (token.tokenType === TokenType.CLOSE_BLOCK_COMMENT) {
153159
// Block comments output a single whitespace positioned at
154160
// the closing slash of the `*/`
161+
// TODO why???? isn't it more intuitive that they should emit nothing???
155162
let virtualSpaceIdx = token.index + 1;
156163
return new Token(TokenType.WHITESPACE, virtualSpaceIdx, virtualSpaceIdx + 1, ' ');
157164
}

src/parsers.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ export function parseFork(lexer: Lexer): ChoiceFork | Reference {
128128

129129
// Big blob in 2nd capture is for identifiers inclusive of non-ascii chars
130130
// It's an approximation of JS identifiers.
131-
let idRe = /(@|#|@!|)([_a-zA-Z\xA0-\uFFFF][_a-zA-Z0-9\xA0-\uFFFF]*)(:?)/y;
131+
let idRe = /(@|#|@!|\$|#\$|)([_a-zA-Z\xA0-\uFFFF][_a-zA-Z0-9\xA0-\uFFFF]*)(:?)/y;
132132

133133
let id = null;
134134
let isReference = false;
135135
let isSilent = false;
136136
let isReExecuting = false;
137+
let isSet = false;
137138

138139
let acceptId = true;
139140
let acceptWeight = false;
@@ -240,7 +241,7 @@ export function parseFork(lexer: Lexer): ChoiceFork | Reference {
240241
if (isReference) {
241242
return new Reference(id!, mappedChoices, unmappedChoices, isReExecuting);
242243
} else {
243-
return new ChoiceFork(unmappedChoices, id, isSilent)
244+
return new ChoiceFork(unmappedChoices, id, isSilent, isSet)
244245
}
245246
} else {
246247
if (!mappedChoices.size && !unmappedChoices.length) {
@@ -256,8 +257,10 @@ export function parseFork(lexer: Lexer): ChoiceFork | Reference {
256257
throw new BMLSyntaxError('Unexpected close brace in fork',
257258
lexer.str, token.index);
258259
}
259-
case TokenType.TEXT:
260260
case TokenType.AT:
261+
case TokenType.HASH:
262+
case TokenType.DOLLAR:
263+
case TokenType.TEXT:
261264
if (acceptId) {
262265
idRe.lastIndex = lexer.index;
263266
let idMatch = idRe.exec(lexer.str);
@@ -281,13 +284,17 @@ export function parseFork(lexer: Lexer): ChoiceFork | Reference {
281284
isReference = true;
282285
isReExecuting = true;
283286
if (includesColon) {
284-
// error
285287
throw new BMLSyntaxError(`Re-executing reference '${id}' should not have a colon, `
286288
+ 'since re-executing references cannot have mappings.',
287289
lexer.str, token.index, `Did you mean '{@!${id}}'?`)
288290
} else {
289291
acceptBlockEnd = true;
290292
}
293+
} else if (typeSlug == '$') {
294+
isSet = true;
295+
} else if (typeSlug == '#$') {
296+
isSet = true;
297+
isSilent = true;
291298
}
292299
lexer.overrideIndex(lexer.index + idMatch[0].length);
293300
acceptId = false;

src/reference.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Choice, WeightedChoice } from './weightedChoice';
22
import { ChoiceFork } from './choiceFork';
3-
import { threadId } from 'worker_threads';
43

54
export type ReferenceMap = Map<number, Choice>;
65

@@ -15,7 +14,7 @@ export class Reference {
1514
this.id = id;
1615
this.referenceMap = choiceMap;
1716
if (fallbackChocies.length) {
18-
this.fallbackChoiceFork = new ChoiceFork(fallbackChocies, null, false);
17+
this.fallbackChoiceFork = new ChoiceFork(fallbackChocies, null, false, false);
1918
} else {
2019
this.fallbackChoiceFork = null;
2120
}

src/renderer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { EvalContext } from './evalBlock';
1414
import * as fileUtils from './fileUtils';
1515

1616

17+
// If the referred fork is a silent set fork that has not yet been executed,
18+
// choiceIndex will be -1 and renderedOutput will be ''.
1719
export type ExecutedFork = { choiceFork: ChoiceFork, choiceIndex: number, renderedOutput: string }
1820
export type ExecutedForkMap = Map<string, ExecutedFork>
1921

src/tokenType.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export enum TokenType {
1818
COMMA = 'COMMA',
1919
COLON = 'COLON',
2020
AT = 'AT',
21+
HASH = 'HASH',
2122
BANG = 'BANG',
23+
DOLLAR = 'DOLLAR',
2224
ARROW = 'ARROW',
2325
NUMBER = 'NUMBER',
2426
TEXT = 'TEXT',

test/testLexer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,24 @@ describe('Lexer', function() {
115115
expect(lexer.next()).toBeNull();
116116
});
117117

118+
it('tokenizes hash', function() {
119+
let lexer = new Lexer('#');
120+
expect(lexer.next()).toEqual(new Token(TokenType.HASH, 0, 1, '#'));
121+
expect(lexer.next()).toBeNull();
122+
});
123+
118124
it('tokenizes bang', function() {
119125
let lexer = new Lexer('!');
120126
expect(lexer.next()).toEqual(new Token(TokenType.BANG, 0, 1, '!'));
121127
expect(lexer.next()).toBeNull();
122128
});
123129

130+
it('tokenizes dollar', function() {
131+
let lexer = new Lexer('$');
132+
expect(lexer.next()).toEqual(new Token(TokenType.DOLLAR, 0, 1, '$'));
133+
expect(lexer.next()).toBeNull();
134+
});
135+
124136
it('tokenizes arrow', function() {
125137
let lexer = new Lexer('->');
126138
expect(lexer.next()).toEqual(new Token(TokenType.ARROW, 0, 2, '->'));

test/testParsers.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,27 @@ describe('parseFork', function() {
180180
expect(result).toBeInstanceOf(ChoiceFork);
181181
expect((result as ChoiceFork).identifier).toBe('TestChoice');
182182
expect((result as ChoiceFork).isSilent).toBe(true);
183+
expect((result as ChoiceFork).isSet).toBe(false);
183184
});
184185

186+
it('allows non-silent sets', function() {
187+
let lexer = new Lexer('$TestChoice: (test)}');
188+
let result = parseFork(lexer);
189+
expect(result).toBeInstanceOf(ChoiceFork);
190+
expect((result as ChoiceFork).identifier).toBe('TestChoice');
191+
expect((result as ChoiceFork).isSilent).toBe(false);
192+
expect((result as ChoiceFork).isSet).toBe(true);
193+
})
194+
195+
it('allows silent sets', function() {
196+
let lexer = new Lexer('#$TestChoice: (test)}');
197+
let result = parseFork(lexer);
198+
expect(result).toBeInstanceOf(ChoiceFork);
199+
expect((result as ChoiceFork).identifier).toBe('TestChoice');
200+
expect((result as ChoiceFork).isSilent).toBe(true);
201+
expect((result as ChoiceFork).isSet).toBe(true);
202+
})
203+
185204
it('allows references', function() {
186205
let lexer = new Lexer('@TestChoice: 0 -> (foo)}');
187206
let result = parseFork(lexer);
@@ -226,11 +245,11 @@ describe('parseFork', function() {
226245
// Nested fork
227246
new ChoiceFork([
228247
new WeightedChoice(['foo'], 100)
229-
], null, false)
248+
], null, false, false)
230249
], 60),
231250
// Alternate branch in outer fork
232251
new WeightedChoice(['bar'], 40)
233-
], null, false);
252+
], null, false, false);
234253
expect(result).toEqual(expectedResult);
235254
});
236255
});

0 commit comments

Comments
 (0)