@lokascript/framework 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -1
- package/dist/api/create-dsl.d.ts +18 -2
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1799 -713
- package/dist/api/index.js.map +1 -1
- package/dist/api/translation-loss.d.ts +84 -0
- package/dist/api/translation-loss.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1057 -684
- package/dist/core/index.js.map +1 -1
- package/dist/core/matching/confidence-model.d.ts +54 -0
- package/dist/core/matching/confidence-model.d.ts.map +1 -0
- package/dist/core/matching/index.d.ts +12 -0
- package/dist/core/matching/index.d.ts.map +1 -0
- package/dist/core/matching/match-skeleton.d.ts +114 -0
- package/dist/core/matching/match-skeleton.d.ts.map +1 -0
- package/dist/core/matching/stock-confidence.d.ts +15 -0
- package/dist/core/matching/stock-confidence.d.ts.map +1 -0
- package/dist/core/matching/stock-reader.d.ts +63 -0
- package/dist/core/matching/stock-reader.d.ts.map +1 -0
- package/dist/core/matching/stock-reading.d.ts +44 -0
- package/dist/core/matching/stock-reading.d.ts.map +1 -0
- package/dist/core/matching/value-reader.d.ts +62 -0
- package/dist/core/matching/value-reader.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +561 -660
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -117
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.d.ts +2 -0
- package/dist/generation/index.d.ts.map +1 -1
- package/dist/generation/index.js +593 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/pattern-render.d.ts +72 -0
- package/dist/generation/pattern-render.d.ts.map +1 -0
- package/dist/generation/schema-patterns.d.ts +152 -0
- package/dist/generation/schema-patterns.d.ts.map +1 -0
- package/dist/index.cjs +2016 -904
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1998 -903
- package/dist/index.js.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/parsing/index.js +561 -660
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/gate/index.d.ts +8 -0
- package/dist/testing/gate/index.d.ts.map +1 -0
- package/dist/testing/gate/index.js +664 -0
- package/dist/testing/gate/index.js.map +1 -0
- package/dist/testing/gate/lint/format.d.ts +7 -0
- package/dist/testing/gate/lint/format.d.ts.map +1 -0
- package/dist/testing/gate/lint/index.d.ts +30 -0
- package/dist/testing/gate/lint/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts +18 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/index.d.ts +14 -0
- package/dist/testing/gate/lint/rules/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts +19 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts +13 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts +20 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts +25 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts +24 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts +10 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts.map +1 -0
- package/dist/testing/gate/lint/types.d.ts +74 -0
- package/dist/testing/gate/lint/types.d.ts.map +1 -0
- package/dist/testing/gate/round-trip.d.ts +184 -0
- package/dist/testing/gate/round-trip.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/__test__/fixtures/sql-dsl.ts +8 -2
- package/src/api/create-dsl-engine.test.ts +150 -0
- package/src/api/create-dsl.test.ts +7 -0
- package/src/api/create-dsl.ts +173 -22
- package/src/api/index.ts +1 -0
- package/src/api/translation-loss.test.ts +89 -0
- package/src/api/translation-loss.ts +128 -0
- package/src/core/index.ts +1 -0
- package/src/core/matching/confidence-model.ts +58 -0
- package/src/core/matching/index.ts +12 -0
- package/src/core/matching/match-skeleton.test.ts +179 -0
- package/src/core/matching/match-skeleton.ts +621 -0
- package/src/core/matching/stock-confidence.ts +238 -0
- package/src/core/matching/stock-reader.test.ts +265 -0
- package/src/core/matching/stock-reader.ts +205 -0
- package/src/core/matching/stock-reading.ts +681 -0
- package/src/core/matching/value-reader.ts +85 -0
- package/src/core/pattern-matching/pattern-matcher.ts +11 -876
- package/src/core/types.ts +53 -0
- package/src/generation/index.ts +2 -0
- package/src/generation/pattern-render.ts +378 -0
- package/src/generation/schema-patterns.test.ts +139 -0
- package/src/generation/schema-patterns.ts +974 -0
- package/src/testing/gate/index.ts +8 -0
- package/src/testing/gate/lint/format.ts +23 -0
- package/src/testing/gate/lint/index.ts +73 -0
- package/src/testing/gate/lint/lint.test.ts +131 -0
- package/src/testing/gate/lint/rules/extractor-coverage.ts +63 -0
- package/src/testing/gate/lint/rules/index.ts +37 -0
- package/src/testing/gate/lint/rules/keyword-classification.ts +52 -0
- package/src/testing/gate/lint/rules/keyword-coverage.ts +38 -0
- package/src/testing/gate/lint/rules/marker-tokenization.ts +129 -0
- package/src/testing/gate/lint/rules/position-ordering.ts +87 -0
- package/src/testing/gate/lint/rules/renderer-coherence.ts +100 -0
- package/src/testing/gate/lint/rules/schema-rules.ts +35 -0
- package/src/testing/gate/lint/types.ts +83 -0
- package/src/testing/gate/round-trip.test.ts +208 -0
- package/src/testing/gate/round-trip.ts +503 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stock confidence model: how framework's old matcher scored a domain
|
|
3
|
+
* pattern's match (`core/pattern-matching/`), moved here as a `ConfidenceModel`
|
|
4
|
+
* for the matcher's skeleton (domains rethink Phase 4.7); the old matcher calls
|
|
5
|
+
* it too until Phase 6.
|
|
6
|
+
*
|
|
7
|
+
* Role coverage (a required slot 1, an optional one 0.8, an absent optional one
|
|
8
|
+
* with a default 60% of that), a stem-match penalty (up to 15%, floor 0.5), and
|
|
9
|
+
* two Arabic adjustments: a VSO verb-first boost and the preposition each role
|
|
10
|
+
* takes most naturally.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { LanguagePattern, SemanticRole, SemanticValue } from '../types';
|
|
14
|
+
import type { ConfidenceBreakdown, ConfidenceContext, ConfidenceModel } from './confidence-model';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculate confidence score for a match (0-1).
|
|
18
|
+
*
|
|
19
|
+
* Confidence is reduced for:
|
|
20
|
+
* - Stem matches (morphological normalization has inherent uncertainty)
|
|
21
|
+
* - Missing optional roles (but less penalty if role has a default value)
|
|
22
|
+
*
|
|
23
|
+
* Confidence is increased for:
|
|
24
|
+
* - VSO languages (Arabic) when pattern starts with a verb
|
|
25
|
+
*/
|
|
26
|
+
function scoreParts(ctx: ConfidenceContext): ConfidenceBreakdown {
|
|
27
|
+
const { pattern, captured } = ctx;
|
|
28
|
+
let score = 0;
|
|
29
|
+
let maxScore = 0;
|
|
30
|
+
|
|
31
|
+
// Helper to check if a role has a default value in extraction rules
|
|
32
|
+
const hasDefault = (role: SemanticRole): boolean => {
|
|
33
|
+
return pattern.extraction?.[role]?.default !== undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Score based on captured roles
|
|
37
|
+
for (const token of pattern.template.tokens) {
|
|
38
|
+
if (token.type === 'role') {
|
|
39
|
+
maxScore += 1;
|
|
40
|
+
if (captured.has(token.role)) {
|
|
41
|
+
score += 1;
|
|
42
|
+
}
|
|
43
|
+
} else if (token.type === 'group') {
|
|
44
|
+
// Group tokens are optional - weight depends on whether they have defaults
|
|
45
|
+
for (const subToken of token.tokens) {
|
|
46
|
+
if (subToken.type === 'role') {
|
|
47
|
+
const roleHasDefault = hasDefault(subToken.role);
|
|
48
|
+
const weight = 0.8; // Optional roles: 80% weight
|
|
49
|
+
maxScore += weight;
|
|
50
|
+
|
|
51
|
+
if (captured.has(subToken.role)) {
|
|
52
|
+
// Role was explicitly provided by user
|
|
53
|
+
score += weight;
|
|
54
|
+
} else if (roleHasDefault) {
|
|
55
|
+
// Role has a default - give 60% partial credit since command is semantically complete
|
|
56
|
+
// This prevents penalizing common patterns like "toggle .active" (default: me)
|
|
57
|
+
score += weight * 0.6;
|
|
58
|
+
}
|
|
59
|
+
// If no default and not captured, score += 0 (true penalty for missing info)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const roleCoverage = maxScore > 0 ? score / maxScore : 1;
|
|
66
|
+
let baseConfidence = roleCoverage;
|
|
67
|
+
let stemPenalty = 0;
|
|
68
|
+
|
|
69
|
+
// Apply penalty for stem matches
|
|
70
|
+
// Each stem match reduces confidence slightly (e.g., 5% per stem match)
|
|
71
|
+
// This ensures exact matches are preferred over morphological matches
|
|
72
|
+
if (ctx.stemMatchCount > 0 && ctx.totalKeywordMatches > 0) {
|
|
73
|
+
stemPenalty = (ctx.stemMatchCount / ctx.totalKeywordMatches) * 0.15;
|
|
74
|
+
baseConfidence = Math.max(0.5, baseConfidence - stemPenalty);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Apply VSO confidence boost for Arabic verb-first patterns
|
|
78
|
+
const vsoBoost = calculateVSOConfidenceBoost(pattern);
|
|
79
|
+
baseConfidence = Math.min(1.0, baseConfidence + vsoBoost);
|
|
80
|
+
|
|
81
|
+
// Apply preposition disambiguation adjustment for Arabic
|
|
82
|
+
const prepositionAdjustment = arabicPrepositionDisambiguation(pattern, captured);
|
|
83
|
+
baseConfidence = Math.max(0.0, Math.min(1.0, baseConfidence + prepositionAdjustment));
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
roleCoverage,
|
|
87
|
+
stemPenalty,
|
|
88
|
+
languageBoost: vsoBoost,
|
|
89
|
+
languageAdjustment: prepositionAdjustment,
|
|
90
|
+
final: baseConfidence,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate confidence boost for VSO (Verb-Subject-Object) language patterns.
|
|
96
|
+
* Arabic naturally uses VSO word order, so patterns that start with a verb
|
|
97
|
+
* should receive a confidence boost.
|
|
98
|
+
*
|
|
99
|
+
* Returns +0.15 confidence boost if:
|
|
100
|
+
* - Language is Arabic ('ar')
|
|
101
|
+
* - Pattern's first token is a verb keyword
|
|
102
|
+
*
|
|
103
|
+
* @param pattern The language pattern being matched
|
|
104
|
+
* @returns Confidence boost (0 or 0.15)
|
|
105
|
+
*/
|
|
106
|
+
function calculateVSOConfidenceBoost(pattern: LanguagePattern): number {
|
|
107
|
+
// Only apply to Arabic
|
|
108
|
+
if (pattern.language !== 'ar') {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if first token in pattern is a literal (keyword)
|
|
113
|
+
const firstToken = pattern.template.tokens[0];
|
|
114
|
+
if (!firstToken || firstToken.type !== 'literal') {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// List of Arabic verb keywords (command verbs)
|
|
119
|
+
const ARABIC_VERBS = new Set([
|
|
120
|
+
'بدل',
|
|
121
|
+
'غير',
|
|
122
|
+
'أضف',
|
|
123
|
+
'أزل',
|
|
124
|
+
'ضع',
|
|
125
|
+
'اجعل',
|
|
126
|
+
'عين',
|
|
127
|
+
'زد',
|
|
128
|
+
'انقص',
|
|
129
|
+
'سجل',
|
|
130
|
+
'أظهر',
|
|
131
|
+
'أخف',
|
|
132
|
+
'شغل',
|
|
133
|
+
'أرسل',
|
|
134
|
+
'ركز',
|
|
135
|
+
'شوش',
|
|
136
|
+
'توقف',
|
|
137
|
+
'انسخ',
|
|
138
|
+
'احذف',
|
|
139
|
+
'اصنع',
|
|
140
|
+
'انتظر',
|
|
141
|
+
'انتقال',
|
|
142
|
+
'أو',
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
// Check if first token value is a verb
|
|
146
|
+
if (ARABIC_VERBS.has(firstToken.value)) {
|
|
147
|
+
return 0.15;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check alternatives
|
|
151
|
+
if (firstToken.alternatives) {
|
|
152
|
+
for (const alt of firstToken.alternatives) {
|
|
153
|
+
if (ARABIC_VERBS.has(alt)) {
|
|
154
|
+
return 0.15;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Arabic preposition disambiguation for confidence adjustment.
|
|
164
|
+
*
|
|
165
|
+
* Different Arabic prepositions are more or less natural for different semantic roles:
|
|
166
|
+
* - على (on/upon) is preferred for patient/target roles (element selectors)
|
|
167
|
+
* - إلى (to) is preferred for destination roles
|
|
168
|
+
* - من (from) is preferred for source roles
|
|
169
|
+
* - في (in) is preferred for location roles
|
|
170
|
+
*
|
|
171
|
+
* This method analyzes the prepositions used with captured semantic roles and
|
|
172
|
+
* adjusts confidence based on idiomaticity:
|
|
173
|
+
* - +0.10 for highly idiomatic preposition choices
|
|
174
|
+
* - -0.10 for less natural preposition choices
|
|
175
|
+
*
|
|
176
|
+
* @param pattern The language pattern being matched
|
|
177
|
+
* @param captured The captured semantic values
|
|
178
|
+
* @returns Confidence adjustment (-0.10 to +0.10)
|
|
179
|
+
*/
|
|
180
|
+
function arabicPrepositionDisambiguation(
|
|
181
|
+
pattern: LanguagePattern,
|
|
182
|
+
captured: ReadonlyMap<SemanticRole, SemanticValue>
|
|
183
|
+
): number {
|
|
184
|
+
// Only apply to Arabic
|
|
185
|
+
if (pattern.language !== 'ar') {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let adjustment = 0;
|
|
190
|
+
|
|
191
|
+
// Preferred prepositions for each semantic role
|
|
192
|
+
// Only including roles that commonly use prepositions in Arabic
|
|
193
|
+
const PREFERRED_PREPOSITIONS: Partial<Record<SemanticRole, string[]>> = {
|
|
194
|
+
patient: ['على'], // element selectors prefer على (on/upon)
|
|
195
|
+
destination: ['إلى', 'الى'], // destination prefers إلى (to)
|
|
196
|
+
source: ['من'], // source prefers من (from)
|
|
197
|
+
agent: ['من'], // agent/by prefers من (from/by)
|
|
198
|
+
manner: ['ب'], // manner prefers ب (with/by)
|
|
199
|
+
style: ['ب'], // style prefers ب (with)
|
|
200
|
+
goal: ['إلى', 'الى'], // target state prefers إلى (to)
|
|
201
|
+
method: ['ب'], // method prefers ب (with/by)
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Check each captured role for preposition metadata
|
|
205
|
+
for (const [role, value] of captured.entries()) {
|
|
206
|
+
// Skip if no preferred prepositions defined for this role
|
|
207
|
+
const preferred = PREFERRED_PREPOSITIONS[role];
|
|
208
|
+
if (!preferred || preferred.length === 0) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if the value has preposition metadata (from Arabic tokenizer)
|
|
213
|
+
// This metadata is attached when a preposition particle token is consumed
|
|
214
|
+
const metadata =
|
|
215
|
+
'metadata' in value ? (value as { metadata: Record<string, unknown> }).metadata : undefined;
|
|
216
|
+
if (metadata && typeof metadata.prepositionValue === 'string') {
|
|
217
|
+
const usedPreposition = metadata.prepositionValue;
|
|
218
|
+
|
|
219
|
+
// Check if the used preposition is in the preferred list
|
|
220
|
+
if (preferred.includes(usedPreposition)) {
|
|
221
|
+
// Idiomatic choice - boost confidence
|
|
222
|
+
adjustment += 0.1;
|
|
223
|
+
} else {
|
|
224
|
+
// Less natural choice - reduce confidence
|
|
225
|
+
adjustment -= 0.1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Cap total adjustment at ±0.10 (even if multiple roles analyzed)
|
|
231
|
+
return Math.max(-0.1, Math.min(0.1, adjustment));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The stock confidence model (stateless: one instance serves every matcher). */
|
|
235
|
+
export const stockConfidenceModel: ConfidenceModel = {
|
|
236
|
+
calculate: ctx => scoreParts(ctx).final,
|
|
237
|
+
calculateWithBreakdown: ctx => scoreParts(ctx),
|
|
238
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stock value reader on the matcher's skeleton (domains rethink Phase 4.7):
|
|
3
|
+
* each kind of value it reads, a greedy slot's stop markers, a slot's word list,
|
|
4
|
+
* the guard against a marker read as a value, and the skip of an optional slot.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { MatchSkeleton } from './match-skeleton';
|
|
8
|
+
import { createStockReader, greedyStopMarkers, readGreedy } from './stock-reader';
|
|
9
|
+
import { stockConfidenceModel } from './stock-confidence';
|
|
10
|
+
import { TokenStreamImpl } from '../tokenization/token-utils';
|
|
11
|
+
import type { LanguagePattern, LanguageToken, PatternToken, TokenKind } from '../types';
|
|
12
|
+
|
|
13
|
+
function stream(...tokens: Array<[string, TokenKind] | [string, TokenKind, string]>) {
|
|
14
|
+
let offset = 0;
|
|
15
|
+
const out: LanguageToken[] = tokens.map(([value, kind, normalized]) => {
|
|
16
|
+
const token: LanguageToken = {
|
|
17
|
+
value,
|
|
18
|
+
kind,
|
|
19
|
+
position: { start: offset, end: offset + value.length },
|
|
20
|
+
...(normalized ? { normalized } : {}),
|
|
21
|
+
};
|
|
22
|
+
offset += value.length + 1;
|
|
23
|
+
return token;
|
|
24
|
+
});
|
|
25
|
+
return new TokenStreamImpl(out, 'en');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lit = (value: string, ...alternatives: string[]): PatternToken =>
|
|
29
|
+
alternatives.length ? { type: 'literal', value, alternatives } : { type: 'literal', value };
|
|
30
|
+
const slot = (
|
|
31
|
+
role: string,
|
|
32
|
+
extra: { optional?: boolean; greedy?: boolean; expectedTypes?: string[]; words?: string[] } = {}
|
|
33
|
+
): PatternToken => ({ type: 'role', role, ...extra }) as PatternToken;
|
|
34
|
+
const group = (...tokens: PatternToken[]): PatternToken => ({
|
|
35
|
+
type: 'group',
|
|
36
|
+
optional: true,
|
|
37
|
+
tokens,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function pattern(tokens: PatternToken[], language = 'en'): LanguagePattern {
|
|
41
|
+
return {
|
|
42
|
+
id: 'p',
|
|
43
|
+
language,
|
|
44
|
+
command: 'go',
|
|
45
|
+
priority: 100,
|
|
46
|
+
template: { format: 'p', tokens },
|
|
47
|
+
extraction: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const matcher = () => new MatchSkeleton(createStockReader(), stockConfidenceModel);
|
|
52
|
+
const read = (p: LanguagePattern, ...tokens: Parameters<typeof stream>) => {
|
|
53
|
+
const result = matcher().matchPattern(stream(...tokens), p);
|
|
54
|
+
return result && Object.fromEntries(result.captured);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('one value per slot', () => {
|
|
58
|
+
const go = pattern([lit('go'), slot('x')]);
|
|
59
|
+
it.each([
|
|
60
|
+
['#box', 'selector', { type: 'selector', value: '#box' }],
|
|
61
|
+
['"hello there"', 'literal', { type: 'literal', value: 'hello there', dataType: 'string' }],
|
|
62
|
+
['42', 'literal', { type: 'literal', value: 42, dataType: 'number' }],
|
|
63
|
+
['true', 'literal', { type: 'literal', value: true, dataType: 'boolean' }],
|
|
64
|
+
['300ms', 'literal', { type: 'literal', value: '300ms', dataType: 'duration' }],
|
|
65
|
+
['/api/users', 'url', { type: 'literal', value: '/api/users', dataType: 'string' }],
|
|
66
|
+
['users', 'identifier', { type: 'expression', raw: 'users' }],
|
|
67
|
+
] as const)('reads %s', (value, kind, want) => {
|
|
68
|
+
expect(read(go, ['go', 'keyword'], [value, kind])).toEqual({ x: want });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('skips an English article before a selector', () => {
|
|
72
|
+
expect(read(go, ['go', 'keyword'], ['the', 'identifier'], ['#box', 'selector'])).toEqual({
|
|
73
|
+
x: { type: 'selector', value: '#box' },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('refuses a value of a type the slot does not take, unless the slot is optional', () => {
|
|
78
|
+
const selectorOnly = pattern([lit('go'), slot('x', { expectedTypes: ['selector'] })]);
|
|
79
|
+
expect(read(selectorOnly, ['go', 'keyword'], ['users', 'identifier'])).toBeNull();
|
|
80
|
+
const optional = pattern([
|
|
81
|
+
lit('go'),
|
|
82
|
+
slot('x', { expectedTypes: ['selector'], optional: true }),
|
|
83
|
+
]);
|
|
84
|
+
expect(read(optional, ['go', 'keyword'], ['users', 'identifier'])).toEqual({});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('a greedy slot', () => {
|
|
89
|
+
it('reads to the next marker the pattern owes', () => {
|
|
90
|
+
const select = pattern([
|
|
91
|
+
lit('select'),
|
|
92
|
+
slot('columns', { greedy: true }),
|
|
93
|
+
lit('from'),
|
|
94
|
+
slot('source'),
|
|
95
|
+
]);
|
|
96
|
+
expect(
|
|
97
|
+
read(
|
|
98
|
+
select,
|
|
99
|
+
['select', 'keyword'],
|
|
100
|
+
['name', 'identifier'],
|
|
101
|
+
[',', 'punctuation'],
|
|
102
|
+
['email', 'identifier'],
|
|
103
|
+
['from', 'keyword'],
|
|
104
|
+
['users', 'identifier']
|
|
105
|
+
)
|
|
106
|
+
).toEqual({
|
|
107
|
+
columns: { type: 'expression', raw: 'name , email' },
|
|
108
|
+
source: { type: 'expression', raw: 'users' },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reads to the end when nothing is owed, and is refused when it reads nothing', () => {
|
|
113
|
+
const where = pattern([lit('where'), slot('condition', { greedy: true })]);
|
|
114
|
+
expect(
|
|
115
|
+
read(where, ['where', 'keyword'], ['age', 'identifier'], ['>', 'operator'], ['18', 'literal'])
|
|
116
|
+
).toEqual({ condition: { type: 'expression', raw: 'age > 18' } });
|
|
117
|
+
expect(read(where, ['where', 'keyword'])).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('stops past an optional group at the required literal after it, by value or normalized form', () => {
|
|
121
|
+
const rest: PatternToken[] = [group(lit('where'), slot('c')), lit('選択', 'select')];
|
|
122
|
+
expect([...greedyStopMarkers(rest)]).toEqual(['where', '選択', 'select']);
|
|
123
|
+
const tokens = stream(['users', 'identifier'], ['SELECT', 'keyword', 'select']);
|
|
124
|
+
expect(readGreedy(tokens, greedyStopMarkers(rest))).toEqual({
|
|
125
|
+
type: 'expression',
|
|
126
|
+
raw: 'users',
|
|
127
|
+
});
|
|
128
|
+
expect(tokens.position()).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('a slot with a word list', () => {
|
|
133
|
+
const click = pattern([lit('when'), slot('action', { words: ['clicks', 'クリック'] })]);
|
|
134
|
+
|
|
135
|
+
it('reads one of its words, in any case, by value or normalized form', () => {
|
|
136
|
+
expect(read(click, ['when', 'keyword'], ['Clicks', 'identifier'])).toEqual({
|
|
137
|
+
action: { type: 'expression', raw: 'Clicks' },
|
|
138
|
+
});
|
|
139
|
+
expect(read(click, ['when', 'keyword'], ['クリックする', 'identifier', 'クリック'])).toEqual({
|
|
140
|
+
action: { type: 'expression', raw: 'クリックする' },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('leaves any other word unread', () => {
|
|
145
|
+
expect(read(click, ['when', 'keyword'], ['zzqx', 'identifier'])).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('gives the word to the required slot when the optional slot before it took it (behaviorspec `when clicks zzqx`)', () => {
|
|
149
|
+
const when = pattern([
|
|
150
|
+
lit('when'),
|
|
151
|
+
group(slot('actor', { optional: true })),
|
|
152
|
+
slot('action', { words: ['clicks'] }),
|
|
153
|
+
]);
|
|
154
|
+
const result = matcher().matchPattern(
|
|
155
|
+
stream(['when', 'keyword'], ['clicks', 'identifier'], ['zzqx', 'identifier']),
|
|
156
|
+
when
|
|
157
|
+
);
|
|
158
|
+
expect(result && Object.fromEntries(result.captured)).toEqual({
|
|
159
|
+
action: { type: 'expression', raw: 'clicks' },
|
|
160
|
+
});
|
|
161
|
+
// `zzqx` is left for the caller to refuse as unread.
|
|
162
|
+
expect(result?.consumedTokens).toBe(2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('an optional one is skipped for a word it does not list (voice `close zzqx`)', () => {
|
|
166
|
+
const close = pattern([lit('close'), slot('patient', { optional: true, words: ['tab'] })]);
|
|
167
|
+
expect(read(close, ['close', 'keyword'], ['tab', 'identifier'])).toEqual({
|
|
168
|
+
patient: { type: 'expression', raw: 'tab' },
|
|
169
|
+
});
|
|
170
|
+
const result = matcher().matchPattern(
|
|
171
|
+
stream(['close', 'keyword'], ['zzqx', 'identifier']),
|
|
172
|
+
close
|
|
173
|
+
);
|
|
174
|
+
expect(result && Object.fromEntries(result.captured)).toEqual({});
|
|
175
|
+
expect(result?.consumedTokens).toBe(1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('the marker guard and the optional skip', () => {
|
|
180
|
+
it('a required slot does not read the marker the pattern wants next', () => {
|
|
181
|
+
// SOV `{patient} を 実行` given `を 実行`: the patient is missing, not `を`.
|
|
182
|
+
const sov = pattern([slot('patient'), lit('を'), lit('実行')], 'ja');
|
|
183
|
+
expect(read(sov, ['を', 'particle'], ['実行', 'keyword'])).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('an optional slot does not read a marker the rest of the pattern owes, through its optional groups', () => {
|
|
187
|
+
// behaviorspec `when user types into #search`: the typed text is optional and has no marker.
|
|
188
|
+
const when = pattern([
|
|
189
|
+
lit('when'),
|
|
190
|
+
slot('action'),
|
|
191
|
+
group(slot('value', { optional: true })),
|
|
192
|
+
group(lit('on'), slot('target')),
|
|
193
|
+
group(lit('into'), slot('destination')),
|
|
194
|
+
]);
|
|
195
|
+
expect(
|
|
196
|
+
read(
|
|
197
|
+
when,
|
|
198
|
+
['when', 'keyword'],
|
|
199
|
+
['types', 'identifier'],
|
|
200
|
+
['into', 'keyword'],
|
|
201
|
+
['#q', 'selector']
|
|
202
|
+
)
|
|
203
|
+
).toEqual({
|
|
204
|
+
action: { type: 'expression', raw: 'types' },
|
|
205
|
+
destination: { type: 'selector', value: '#q' },
|
|
206
|
+
});
|
|
207
|
+
expect(
|
|
208
|
+
read(
|
|
209
|
+
when,
|
|
210
|
+
['when', 'keyword'],
|
|
211
|
+
['types', 'identifier'],
|
|
212
|
+
["'hi'", 'literal'],
|
|
213
|
+
['into', 'keyword'],
|
|
214
|
+
['#q', 'selector']
|
|
215
|
+
)
|
|
216
|
+
).toEqual({
|
|
217
|
+
action: { type: 'expression', raw: 'types' },
|
|
218
|
+
value: { type: 'literal', value: 'hi', dataType: 'string' },
|
|
219
|
+
destination: { type: 'selector', value: '#q' },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('an optional slot is skipped for the literal the pattern wants next, when the rest then reads to the end', () => {
|
|
224
|
+
const p = pattern([lit('go'), group(slot('x', { optional: true })), lit('now')]);
|
|
225
|
+
expect(read(p, ['go', 'keyword'], ['now', 'keyword'])).toEqual({});
|
|
226
|
+
expect(read(p, ['go', 'keyword'], ['#a', 'selector'], ['now', 'keyword'])).toEqual({
|
|
227
|
+
x: { type: 'selector', value: '#a' },
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('a required slot takes back the optional slot before it', () => {
|
|
233
|
+
it('when the optional slot took its value (behaviorspec `when clicks`)', () => {
|
|
234
|
+
const when = pattern([lit('when'), group(slot('actor', { optional: true })), slot('action')]);
|
|
235
|
+
expect(read(when, ['when', 'keyword'], ['clicks', 'identifier'])).toEqual({
|
|
236
|
+
action: { type: 'expression', raw: 'clicks' },
|
|
237
|
+
});
|
|
238
|
+
expect(
|
|
239
|
+
read(when, ['when', 'keyword'], ['user', 'identifier'], ['clicks', 'identifier'])
|
|
240
|
+
).toEqual({
|
|
241
|
+
actor: { type: 'expression', raw: 'user' },
|
|
242
|
+
action: { type: 'expression', raw: 'clicks' },
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('when it stands at its own marker (flow `orders として 取得変数`)', () => {
|
|
247
|
+
const capture = pattern(
|
|
248
|
+
[
|
|
249
|
+
group(slot('patient', { optional: true })),
|
|
250
|
+
slot('destination'),
|
|
251
|
+
lit('として'),
|
|
252
|
+
lit('取得変数'),
|
|
253
|
+
],
|
|
254
|
+
'ja'
|
|
255
|
+
);
|
|
256
|
+
expect(
|
|
257
|
+
read(capture, ['orders', 'identifier'], ['として', 'particle'], ['取得変数', 'keyword'])
|
|
258
|
+
).toEqual({ destination: { type: 'expression', raw: 'orders' } });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('not when something was read between them', () => {
|
|
262
|
+
const p = pattern([group(slot('a', { optional: true })), lit('to'), slot('b')]);
|
|
263
|
+
expect(read(p, ['x', 'identifier'], ['to', 'keyword'])).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|