@@ -8,20 +8,83 @@ import {
88
99export const AUTO_INCREMENT_IDENTIFIER = Symbol ( 'auto-increment' ) ;
1010
11- export function assignSequenceIdentifiers ( message : Message , sequence = { current : 0 } ) {
12- if (
13- message instanceof ArgumentMessage ||
14- message instanceof ElementMessage ||
15- message instanceof ChoiceMessage
16- )
17- if ( message . identifier === AUTO_INCREMENT_IDENTIFIER )
18- message . identifier = `${ sequence . current ++ } ` ;
19- if ( message instanceof CompositeMessage || message instanceof ElementMessage )
20- for ( const child of message . children ) assignSequenceIdentifiers ( child , sequence ) ;
21- if ( message instanceof ChoiceMessage )
22- for ( const branch of message . branches ) {
23- if ( branch . identifier === AUTO_INCREMENT_IDENTIFIER )
24- branch . identifier = `${ sequence . current ++ } ` ;
25- assignSequenceIdentifiers ( branch . value , sequence ) ;
11+ /**
12+ * Decides whether two elements sharing a tag are the same element, and so may
13+ * share it. Only the syntax that produced them can answer that, so the caller
14+ * supplies the comparison; by default no two elements are interchangeable.
15+ */
16+ export type ElementEquivalence = ( a : any , b : any ) => boolean ;
17+
18+ export function assignSequenceIdentifiers (
19+ message : Message ,
20+ sequence = { current : 0 } ,
21+ equivalent : ElementEquivalence = ( ) => false ,
22+ ) {
23+ const reserved = collectAssignedIdentifiers ( message , equivalent ) ;
24+
25+ function next ( ) {
26+ let identifier = `${ sequence . current ++ } ` ;
27+ while ( reserved . has ( identifier ) ) identifier = `${ sequence . current ++ } ` ;
28+ return identifier ;
29+ }
30+
31+ function walk ( message : Message ) {
32+ if (
33+ message instanceof ArgumentMessage ||
34+ message instanceof ElementMessage ||
35+ message instanceof ChoiceMessage
36+ )
37+ if ( message . identifier === AUTO_INCREMENT_IDENTIFIER ) message . identifier = next ( ) ;
38+ if ( message instanceof CompositeMessage || message instanceof ElementMessage )
39+ for ( const child of message . children ) walk ( child ) ;
40+ if ( message instanceof ChoiceMessage )
41+ for ( const branch of message . branches ) {
42+ if ( branch . identifier === AUTO_INCREMENT_IDENTIFIER ) branch . identifier = next ( ) ;
43+ walk ( branch . value ) ;
44+ }
45+ }
46+
47+ walk ( message ) ;
48+ }
49+
50+ /**
51+ * Collect the identifiers already assigned before this pass runs, so generated
52+ * sequence numbers never shadow an explicit one (e.g. an element tagged `0`).
53+ *
54+ * Elements that differ need their own tag: they each compile to their own prop,
55+ * and a translator has to be able to tell them apart. Repeats are fine when
56+ * nothing distinguishes them — two identical elements are one prop, the same
57+ * way the same variable interpolated twice is one value — so arguments are
58+ * never checked and elements only when they are not equivalent.
59+ */
60+ function collectAssignedIdentifiers ( message : Message , equivalent : ElementEquivalence ) {
61+ const tags = new Map < string , ElementMessage > ( ) ;
62+ const values = new Set < string > ( ) ;
63+
64+ function walk ( message : Message ) {
65+ if ( message instanceof ElementMessage && typeof message . identifier === 'string' ) {
66+ const previous = tags . get ( message . identifier ) ;
67+ if ( previous && ! equivalent ( previous . expression , message . expression ) )
68+ throw new Error (
69+ `Duplicate element tag '${ message . identifier } ', give each element in a message its own tag unless they are identical` ,
70+ ) ;
71+ tags . set ( message . identifier , message ) ;
2672 }
73+ if (
74+ ( message instanceof ArgumentMessage || message instanceof ChoiceMessage ) &&
75+ typeof message . identifier === 'string'
76+ )
77+ values . add ( message . identifier ) ;
78+ if ( message instanceof CompositeMessage || message instanceof ElementMessage )
79+ for ( const child of message . children ) walk ( child ) ;
80+ if ( message instanceof ChoiceMessage ) for ( const branch of message . branches ) walk ( branch . value ) ;
81+ }
82+
83+ walk ( message ) ;
84+
85+ for ( const tag of tags . keys ( ) )
86+ if ( values . has ( tag ) )
87+ throw new Error ( `Element tag '${ tag } ' collides with an argument of the same name` ) ;
88+
89+ return new Set ( [ ...tags . keys ( ) , ...values ] ) ;
2790}
0 commit comments