@lokascript/framework 4.7.0 → 5.0.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 +76 -1
- package/dist/api/create-dsl.d.ts +14 -12
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/index.js +888 -1683
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +560 -1133
- package/dist/core/index.js.map +1 -1
- package/dist/core/matching/index.d.ts +2 -0
- package/dist/core/matching/index.d.ts.map +1 -1
- package/dist/core/matching/possessive-keywords.d.ts.map +1 -0
- package/dist/core/matching/stock-confidence.d.ts +3 -4
- package/dist/core/matching/stock-confidence.d.ts.map +1 -1
- package/dist/core/matching/stock-reader.d.ts +8 -1
- package/dist/core/matching/stock-reader.d.ts.map +1 -1
- package/dist/core/matching/stock-reading.d.ts +4 -4
- package/dist/core/{pattern-matching/utils → matching}/type-validation.d.ts +1 -1
- package/dist/core/matching/type-validation.d.ts.map +1 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.d.ts +1 -1
- package/dist/generation/index.d.ts.map +1 -1
- package/dist/generation/index.js +32 -171
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/profile.d.ts +30 -0
- package/dist/generation/profile.d.ts.map +1 -0
- package/dist/generation/renderer.d.ts +1 -1
- package/dist/generation/renderer.d.ts.map +1 -1
- package/dist/generation/schema-patterns.d.ts +3 -3
- package/dist/index.cjs +969 -1674
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +969 -1668
- package/dist/index.js.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/multilingual/builders.d.ts +3 -3
- package/dist/multilingual/builders.d.ts.map +1 -1
- package/dist/multilingual/index.js.map +1 -1
- package/dist/parsing/index.d.ts +0 -1
- package/dist/parsing/index.d.ts.map +1 -1
- package/dist/parsing/index.js +0 -1145
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/gate/index.js +44 -12
- package/dist/testing/gate/index.js.map +1 -1
- package/dist/testing/gate/lint/types.d.ts +1 -1
- package/dist/testing/gate/lint/types.d.ts.map +1 -1
- package/dist/testing/gate/round-trip.d.ts +24 -3
- package/dist/testing/gate/round-trip.d.ts.map +1 -1
- package/package.json +2 -7
- package/src/__test__/test-utils.ts +1 -1
- package/src/api/create-dsl-engine.test.ts +123 -34
- package/src/api/create-dsl.test.ts +7 -0
- package/src/api/create-dsl.ts +56 -39
- package/src/core/index.ts +0 -1
- package/src/core/matching/index.ts +2 -0
- package/src/core/matching/stock-confidence.ts +3 -4
- package/src/core/matching/stock-reader.test.ts +87 -3
- package/src/core/matching/stock-reader.ts +20 -1
- package/src/core/matching/stock-reading.ts +6 -6
- package/src/core/{pattern-matching/utils → matching}/type-validation.test.ts +2 -2
- package/src/core/{pattern-matching/utils → matching}/type-validation.ts +1 -1
- package/src/core/types.ts +5 -0
- package/src/generation/domain-renderer.test.ts +1 -1
- package/src/generation/index.ts +1 -1
- package/src/generation/profile.ts +41 -0
- package/src/generation/renderer.test.ts +1 -1
- package/src/generation/renderer.ts +1 -1
- package/src/generation/schema-patterns.test.ts +41 -1
- package/src/generation/schema-patterns.ts +15 -3
- package/src/multilingual/bridge.test.ts +31 -23
- package/src/multilingual/builders.ts +3 -3
- package/src/parsing/index.ts +0 -1
- package/src/testing/gate/lint/types.ts +1 -1
- package/src/testing/gate/round-trip.test.ts +99 -9
- package/src/testing/gate/round-trip.ts +76 -11
- package/dist/core/pattern-matching/index.d.ts +0 -6
- package/dist/core/pattern-matching/index.d.ts.map +0 -1
- package/dist/core/pattern-matching/index.js +0 -1211
- package/dist/core/pattern-matching/index.js.map +0 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -122
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/index.d.ts +0 -6
- package/dist/core/pattern-matching/utils/index.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/type-validation.d.ts.map +0 -1
- package/dist/generation/pattern-generator.d.ts +0 -55
- package/dist/generation/pattern-generator.d.ts.map +0 -1
- package/src/core/pattern-matching/index.ts +0 -6
- package/src/core/pattern-matching/pattern-matcher.test.ts +0 -1031
- package/src/core/pattern-matching/pattern-matcher.ts +0 -765
- package/src/core/pattern-matching/utils/index.ts +0 -6
- package/src/generation/pattern-generator.test.ts +0 -532
- package/src/generation/pattern-generator.ts +0 -327
- /package/dist/core/{pattern-matching/utils → matching}/possessive-keywords.d.ts +0 -0
- /package/src/core/{pattern-matching/utils → matching}/possessive-keywords.ts +0 -0
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The core engine (domains rethink Phase 4.7), the only engine since 5.0. framework's SQL fixture
|
|
3
|
+
* reads, compiles and translates as the classic engine did (its readings and translations below
|
|
4
|
+
* were taken from the classic engine on 4.8), refuses what it does not read, and renders through
|
|
5
|
+
* its own patterns where no renderer is configured. A role with a word list reads only its words.
|
|
6
6
|
*/
|
|
7
7
|
import { describe, expect, it, vi } from 'vitest';
|
|
8
8
|
import { createMultilingualDSL, UnreadInputError, type DSLConfig } from './create-dsl';
|
|
9
|
+
import { defineCommand, defineRole } from '../schema';
|
|
10
|
+
import { BaseTokenizer, getDefaultExtractors } from '../index';
|
|
9
11
|
import { sqlDSLConfig, testQueries, expectedSQL } from '../__test__/fixtures/sql-dsl';
|
|
10
12
|
import type { SemanticNode } from '../core/types';
|
|
11
|
-
import { generatePattern } from '../generation/pattern-generator';
|
|
12
13
|
import { createSchemaPatternGenerator } from '../generation/schema-patterns';
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
// these are what show which engine a DSL was built on.
|
|
16
|
-
vi.mock('../generation/pattern-generator', async importOriginal => {
|
|
17
|
-
const actual = await importOriginal<typeof import('../generation/pattern-generator')>();
|
|
18
|
-
return { ...actual, generatePattern: vi.fn(actual.generatePattern) };
|
|
19
|
-
});
|
|
15
|
+
// A pass-through spy: what shows a DSL was built on the core.
|
|
20
16
|
vi.mock('../generation/schema-patterns', async importOriginal => {
|
|
21
17
|
const actual = await importOriginal<typeof import('../generation/schema-patterns')>();
|
|
22
18
|
return { ...actual, createSchemaPatternGenerator: vi.fn(actual.createSchemaPatternGenerator) };
|
|
23
19
|
});
|
|
24
20
|
|
|
25
|
-
const core = createMultilingualDSL(
|
|
26
|
-
const classic = createMultilingualDSL(sqlDSLConfig());
|
|
21
|
+
const core = createMultilingualDSL(sqlDSLConfig());
|
|
27
22
|
|
|
28
23
|
const reading = (node: SemanticNode) =>
|
|
29
24
|
[
|
|
@@ -34,23 +29,54 @@ const reading = (node: SemanticNode) =>
|
|
|
34
29
|
),
|
|
35
30
|
].join(' ');
|
|
36
31
|
|
|
37
|
-
describe(
|
|
32
|
+
describe('createMultilingualDSL on the core engine', () => {
|
|
33
|
+
// [language, input, SQL, reading, translation] — the reading and translation the classic engine gave.
|
|
38
34
|
const queries = [
|
|
39
|
-
[
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
[
|
|
36
|
+
'en',
|
|
37
|
+
testQueries.english.simple,
|
|
38
|
+
expectedSQL.simple,
|
|
39
|
+
'select columns=name source=users',
|
|
40
|
+
'seleccionar name de users',
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
'en',
|
|
44
|
+
testQueries.english.withWhere,
|
|
45
|
+
expectedSQL.withWhere,
|
|
46
|
+
'select columns=name source=users condition=age > 18',
|
|
47
|
+
'seleccionar name de users donde age > 18',
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'es',
|
|
51
|
+
testQueries.spanish.simple,
|
|
52
|
+
'SELECT nombre FROM usuarios',
|
|
53
|
+
'select columns=nombre source=usuarios',
|
|
54
|
+
'select nombre from usuarios',
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'es',
|
|
58
|
+
testQueries.spanish.withWhere,
|
|
59
|
+
'SELECT nombre FROM usuarios WHERE edad > 18',
|
|
60
|
+
'select columns=nombre source=usuarios condition=edad > 18',
|
|
61
|
+
'select nombre from usuarios where edad > 18',
|
|
62
|
+
],
|
|
43
63
|
] as const;
|
|
44
64
|
|
|
45
|
-
it.each(queries)(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
it.each(queries)(
|
|
66
|
+
'reads and compiles %s `%s` as the classic engine did',
|
|
67
|
+
(lang, input, sql, read) => {
|
|
68
|
+
expect(reading(core.parse(input, lang))).toBe(read);
|
|
69
|
+
expect(core.compile(input, lang).code).toBe(sql);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
49
72
|
|
|
50
|
-
it.each(queries)(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
it.each(queries)(
|
|
74
|
+
'translates %s `%s` as the classic engine did',
|
|
75
|
+
(lang, input, _sql, _read, translated) => {
|
|
76
|
+
const to = lang === 'en' ? 'es' : 'en';
|
|
77
|
+
expect(core.translate(input, lang, to)).toBe(translated);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
54
80
|
|
|
55
81
|
it('refuses input it does not read', () => {
|
|
56
82
|
expect(() => core.parse('select name from users banana', 'en')).toThrow(UnreadInputError);
|
|
@@ -68,19 +94,82 @@ describe("createMultilingualDSL with engine: 'core'", () => {
|
|
|
68
94
|
});
|
|
69
95
|
|
|
70
96
|
it.each([
|
|
71
|
-
['
|
|
72
|
-
['
|
|
73
|
-
|
|
74
|
-
] as const)('builds on the %s engine with %o', (expected, option) => {
|
|
75
|
-
vi.mocked(generatePattern).mockClear();
|
|
97
|
+
['no engine option', {}],
|
|
98
|
+
["engine: 'core'", { engine: 'core' }],
|
|
99
|
+
] as const)('builds on the core with %s', (_label, option) => {
|
|
76
100
|
vi.mocked(createSchemaPatternGenerator).mockClear();
|
|
77
101
|
createMultilingualDSL({ ...sqlDSLConfig(), ...option }).parse(testQueries.english.simple, 'en');
|
|
78
|
-
expect(vi.mocked(createSchemaPatternGenerator).mock.calls.length
|
|
79
|
-
expect(vi.mocked(generatePattern).mock.calls.length > 0).toBe(expected === 'classic');
|
|
102
|
+
expect(vi.mocked(createSchemaPatternGenerator).mock.calls.length).toBeGreaterThan(0);
|
|
80
103
|
});
|
|
81
104
|
|
|
82
|
-
it(
|
|
105
|
+
it("refuses engine: 'classic', removed in 5.0, and says what replaced it", () => {
|
|
106
|
+
const classic = { ...sqlDSLConfig(), engine: 'classic' } as unknown as DSLConfig;
|
|
107
|
+
expect(() => createMultilingualDSL(classic)).toThrow(/removed in @lokascript\/framework 5\.0/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('throws on an engine it does not know, rather than reading with the default', () => {
|
|
83
111
|
const misspelled = { ...sqlDSLConfig(), engine: 'cor' } as unknown as DSLConfig;
|
|
84
112
|
expect(() => createMultilingualDSL(misspelled)).toThrow('Unknown DSL engine "cor"');
|
|
85
113
|
});
|
|
86
114
|
});
|
|
115
|
+
|
|
116
|
+
describe('a role with a word list, on the core', () => {
|
|
117
|
+
class WordTokenizer extends BaseTokenizer {
|
|
118
|
+
readonly language = 'en';
|
|
119
|
+
readonly direction = 'ltr' as const;
|
|
120
|
+
constructor() {
|
|
121
|
+
super();
|
|
122
|
+
this.registerExtractors(getDefaultExtractors());
|
|
123
|
+
}
|
|
124
|
+
classifyToken(token: string): 'keyword' | 'identifier' {
|
|
125
|
+
return token.toLowerCase() === 'when' ? 'keyword' : 'identifier';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// behaviorspec's `when [actor] action`: both are bare words.
|
|
129
|
+
const when = defineCommand({
|
|
130
|
+
action: 'when',
|
|
131
|
+
primaryRole: 'action',
|
|
132
|
+
roles: [
|
|
133
|
+
defineRole({ role: 'actor', required: false, expectedTypes: ['expression'], svoPosition: 2 }),
|
|
134
|
+
defineRole({
|
|
135
|
+
role: 'action',
|
|
136
|
+
required: true,
|
|
137
|
+
expectedTypes: ['expression'],
|
|
138
|
+
svoPosition: 1,
|
|
139
|
+
words: { '*': ['clicks', 'types'] },
|
|
140
|
+
}),
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
const dsl = createMultilingualDSL({
|
|
144
|
+
name: 'when',
|
|
145
|
+
schemas: [when],
|
|
146
|
+
languages: [
|
|
147
|
+
{
|
|
148
|
+
code: 'en',
|
|
149
|
+
name: 'English',
|
|
150
|
+
nativeName: 'English',
|
|
151
|
+
tokenizer: new WordTokenizer(),
|
|
152
|
+
patternProfile: {
|
|
153
|
+
code: 'en',
|
|
154
|
+
wordOrder: 'SVO',
|
|
155
|
+
keywords: { when: { primary: 'when' } },
|
|
156
|
+
roleMarkers: {},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
engine: 'core',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('reads a listed word, with or without the optional role before it', () => {
|
|
164
|
+
expect(reading(dsl.parse('when clicks', 'en'))).toBe('when action=clicks');
|
|
165
|
+
expect(reading(dsl.parse('when user clicks', 'en'))).toBe('when actor=user action=clicks');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('refuses a word it does not list, rather than reading the listed word as the role before it', () => {
|
|
169
|
+
// Without the list: actor `clicks`, action `zzqx`.
|
|
170
|
+
expect(() => dsl.parse('when clicks zzqx', 'en')).toThrow(UnreadInputError);
|
|
171
|
+
// A listed word is not part of a longer value, so the refusal does not say to quote it.
|
|
172
|
+
expect(() => dsl.parse('when clicks zzqx', 'en')).toThrow(/leaving "zzqx" unread$/);
|
|
173
|
+
expect(() => dsl.parse('when user frobs', 'en')).toThrow();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -449,6 +449,13 @@ describe('MultilingualDSL: Explicit Syntax Support', () => {
|
|
|
449
449
|
expect(refusal.message).toMatch(/leaving "banana" unread/);
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
+
it('says when the bare word read last and the next may be one value, to be quoted', () => {
|
|
453
|
+
const dsl = createTestDSL();
|
|
454
|
+
expect(() => dsl.parse('select name from users banana', 'en')).toThrow(
|
|
455
|
+
/unread; if "users banana" is one value, quote it$/
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
452
459
|
it('compile and validate report the refusal instead of compiling the shorter reading', () => {
|
|
453
460
|
const dsl = createTestDSL();
|
|
454
461
|
const compiled = dsl.compile('select name from users banana banana', 'en');
|
package/src/api/create-dsl.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
SemanticNode,
|
|
7
|
+
LanguageToken,
|
|
7
8
|
LanguageTokenizer,
|
|
8
9
|
LanguagePattern,
|
|
9
10
|
PatternMatchResult,
|
|
@@ -12,10 +13,9 @@ import type {
|
|
|
12
13
|
} from '../core/types';
|
|
13
14
|
import { extractRoleValue } from '../core/types';
|
|
14
15
|
import type { CommandSchema } from '../schema';
|
|
15
|
-
import { PatternMatcher } from '../core/pattern-matching';
|
|
16
16
|
import { MatchSkeleton, createStockReader, stockConfidenceModel } from '../core/matching';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import type { PatternGenLanguageProfile } from '../generation/profile';
|
|
18
|
+
import type { DomainRenderFn } from '../generation/renderer';
|
|
19
19
|
import {
|
|
20
20
|
createSchemaPatternGenerator,
|
|
21
21
|
type SchemaPatternProfile,
|
|
@@ -185,18 +185,17 @@ export interface DSLConfig {
|
|
|
185
185
|
readonly customPatterns?: LanguagePattern[];
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
* What reads and writes the DSL
|
|
189
|
-
* default:
|
|
190
|
-
* `'core'`: the core semantic's hyperscript parser runs — patterns from
|
|
188
|
+
* What reads and writes the DSL. `'core'`, the only engine (and the
|
|
189
|
+
* default): the core semantic's hyperscript parser runs — patterns from
|
|
191
190
|
* `createSchemaPatternGenerator` (positions descending, as a domain's schemas
|
|
192
191
|
* read), matched by `MatchSkeleton` with the stock reader and confidence
|
|
193
192
|
* model, and rendered through those patterns where the domain's own renderer
|
|
194
|
-
* declines.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
193
|
+
* declines. The option stays so a config written for framework 4.7–4.8
|
|
194
|
+
* (`engine: 'core'`) builds unchanged; `'classic'`, framework's first
|
|
195
|
+
* generator and matcher, was removed in 5.0 (domains rethink Phase 6) and
|
|
196
|
+
* throws.
|
|
198
197
|
*/
|
|
199
|
-
readonly engine?: '
|
|
198
|
+
readonly engine?: 'core';
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
/**
|
|
@@ -229,7 +228,9 @@ export interface CompileResult {
|
|
|
229
228
|
/**
|
|
230
229
|
* Thrown by `parse` (and so `compile`, `validate`, `translate`) when the input reads as a command
|
|
231
230
|
* only by leaving some of it unread: `select name from users banana` is not `select name from users`.
|
|
232
|
-
* `reading` is what the longest such match read, `unread` the text after it.
|
|
231
|
+
* `reading` is what the longest such match read, `unread` the text after it. `hint`, when there is
|
|
232
|
+
* one, says how the input might read: where a bare word read as a value is followed by another, the
|
|
233
|
+
* two may be one value written without quotes (todo `add buy milk to groceries`).
|
|
233
234
|
*/
|
|
234
235
|
export class UnreadInputError extends Error {
|
|
235
236
|
readonly code = 'unread-input';
|
|
@@ -238,9 +239,13 @@ export class UnreadInputError extends Error {
|
|
|
238
239
|
readonly input: string,
|
|
239
240
|
readonly language: string,
|
|
240
241
|
readonly reading: SemanticNode,
|
|
241
|
-
readonly unread: string
|
|
242
|
+
readonly unread: string,
|
|
243
|
+
readonly hint?: string
|
|
242
244
|
) {
|
|
243
|
-
super(
|
|
245
|
+
super(
|
|
246
|
+
`"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread` +
|
|
247
|
+
(hint ? `; ${hint}` : '')
|
|
248
|
+
);
|
|
244
249
|
this.name = 'UnreadInputError';
|
|
245
250
|
}
|
|
246
251
|
}
|
|
@@ -491,16 +496,37 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
491
496
|
.slice(longestPartial.consumedTokens)
|
|
492
497
|
.map(t => t.value)
|
|
493
498
|
.join(' ');
|
|
499
|
+
const hint = this.quoteHint(longestPartial, tokens.tokens);
|
|
494
500
|
throw new UnreadInputError(
|
|
495
501
|
input,
|
|
496
502
|
language,
|
|
497
503
|
this.nodeOf(longestPartial, input, language),
|
|
498
|
-
unread
|
|
504
|
+
unread,
|
|
505
|
+
...(hint ? [hint] : [])
|
|
499
506
|
);
|
|
500
507
|
}
|
|
501
508
|
throw new Error(`No pattern matched for input: ${input}`);
|
|
502
509
|
}
|
|
503
510
|
|
|
511
|
+
/**
|
|
512
|
+
* `if "buy milk" is one value, quote it`, where the last token a match read is a bare word it took
|
|
513
|
+
* as a role's value (a role with no word list) and the first it left is a bare word too.
|
|
514
|
+
*/
|
|
515
|
+
private quoteHint(
|
|
516
|
+
match: PatternMatchResult,
|
|
517
|
+
tokens: readonly LanguageToken[]
|
|
518
|
+
): string | undefined {
|
|
519
|
+
const last = tokens[match.consumedTokens - 1];
|
|
520
|
+
const next = tokens[match.consumedTokens];
|
|
521
|
+
if (last?.kind !== 'identifier' || next?.kind !== 'identifier') return undefined;
|
|
522
|
+
const schema = this.schemaLookup.getSchema(match.pattern.command);
|
|
523
|
+
const role = [...match.captured].find(
|
|
524
|
+
([, v]) => (v as { raw?: unknown }).raw === last.value
|
|
525
|
+
)?.[0];
|
|
526
|
+
if (!role || schema?.roles.find(r => r.role === role)?.words) return undefined;
|
|
527
|
+
return `if "${last.value} ${next.value}" is one value, quote it`;
|
|
528
|
+
}
|
|
529
|
+
|
|
504
530
|
private nodeOf(match: PatternMatchResult, input: string, language: string): SemanticNode {
|
|
505
531
|
return {
|
|
506
532
|
kind: 'command',
|
|
@@ -860,10 +886,10 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
|
860
886
|
// =============================================================================
|
|
861
887
|
|
|
862
888
|
/**
|
|
863
|
-
* What
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
889
|
+
* What reads and writes a DSL: how each command's pattern is generated, how
|
|
890
|
+
* input is matched against it, and the renderer `render()` falls back to.
|
|
891
|
+
* Parsing's whole-input rule, refusals, translation's read-back, compile,
|
|
892
|
+
* explicit syntax and extensions sit above it.
|
|
867
893
|
*/
|
|
868
894
|
interface DSLEngine {
|
|
869
895
|
generate(schema: CommandSchema, profile: PatternGenLanguageProfile): LanguagePattern;
|
|
@@ -872,29 +898,20 @@ interface DSLEngine {
|
|
|
872
898
|
}
|
|
873
899
|
|
|
874
900
|
/**
|
|
875
|
-
* The engine `config.engine` names
|
|
876
|
-
* with the default
|
|
877
|
-
*
|
|
901
|
+
* The engine `config.engine` names: the core, the only one since 5.0. An
|
|
902
|
+
* unknown name throws rather than reading with the default, and `'classic'`
|
|
903
|
+
* says what replaced it.
|
|
878
904
|
*/
|
|
879
905
|
function engineFor(config: DSLConfig): DSLEngine {
|
|
880
|
-
const name = config.engine ?? '
|
|
881
|
-
if (name === 'classic') return classicEngine();
|
|
906
|
+
const name: string = config.engine ?? 'core';
|
|
882
907
|
if (name === 'core') return coreEngine();
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
generate: (schema, profile) => generatePattern(schema, profile),
|
|
891
|
-
match: (tokens, pattern, language) => matcher.matchPattern(tokens, pattern, { code: language }),
|
|
892
|
-
fallbackRenderer: config =>
|
|
893
|
-
createDomainRenderer({
|
|
894
|
-
schemas: config.schemas,
|
|
895
|
-
profiles: config.languages.map(l => l.patternProfile),
|
|
896
|
-
}),
|
|
897
|
-
};
|
|
908
|
+
if (name === 'classic') {
|
|
909
|
+
throw new Error(
|
|
910
|
+
'The "classic" DSL engine was removed in @lokascript/framework 5.0. ' +
|
|
911
|
+
'Omit `engine` (or pass "core"): every domain reads on the core.'
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
throw new Error(`Unknown DSL engine "${name}" (expected "core")`);
|
|
898
915
|
}
|
|
899
916
|
|
|
900
917
|
/**
|
package/src/core/index.ts
CHANGED
|
@@ -10,3 +10,5 @@ export type { ConfidenceBreakdown, ConfidenceContext, ConfidenceModel } from './
|
|
|
10
10
|
export { StockValueReader, createStockReader, greedyStopMarkers, readGreedy } from './stock-reader';
|
|
11
11
|
export { readRoleValue, type ReadingProfile } from './stock-reading';
|
|
12
12
|
export { stockConfidenceModel } from './stock-confidence';
|
|
13
|
+
export * from './type-validation';
|
|
14
|
+
export * from './possessive-keywords';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The stock confidence model: how framework's
|
|
3
|
-
* pattern's match
|
|
4
|
-
*
|
|
5
|
-
* it too until Phase 6.
|
|
2
|
+
* The stock confidence model: how framework's first matcher scored a domain
|
|
3
|
+
* pattern's match, moved here as a `ConfidenceModel` for the matcher's
|
|
4
|
+
* skeleton (domains rethink Phase 4.7).
|
|
6
5
|
*
|
|
7
6
|
* Role coverage (a required slot 1, an optional one 0.8, an absent optional one
|
|
8
7
|
* with a default 60% of that), a stem-match penalty (up to 15%, floor 0.5), and
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
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,
|
|
4
|
-
* marker read as a value, and the skip of an optional slot.
|
|
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
5
|
*/
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { MatchSkeleton } from './match-skeleton';
|
|
@@ -29,7 +29,7 @@ const lit = (value: string, ...alternatives: string[]): PatternToken =>
|
|
|
29
29
|
alternatives.length ? { type: 'literal', value, alternatives } : { type: 'literal', value };
|
|
30
30
|
const slot = (
|
|
31
31
|
role: string,
|
|
32
|
-
extra: { optional?: boolean; greedy?: boolean; expectedTypes?: string[] } = {}
|
|
32
|
+
extra: { optional?: boolean; greedy?: boolean; expectedTypes?: string[]; words?: string[] } = {}
|
|
33
33
|
): PatternToken => ({ type: 'role', role, ...extra }) as PatternToken;
|
|
34
34
|
const group = (...tokens: PatternToken[]): PatternToken => ({
|
|
35
35
|
type: 'group',
|
|
@@ -129,6 +129,53 @@ describe('a greedy slot', () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
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
|
+
|
|
132
179
|
describe('the marker guard and the optional skip', () => {
|
|
133
180
|
it('a required slot does not read the marker the pattern wants next', () => {
|
|
134
181
|
// SOV `{patient} を 実行` given `を 実行`: the patient is missing, not `を`.
|
|
@@ -136,6 +183,43 @@ describe('the marker guard and the optional skip', () => {
|
|
|
136
183
|
expect(read(sov, ['を', 'particle'], ['実行', 'keyword'])).toBeNull();
|
|
137
184
|
});
|
|
138
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
|
+
|
|
139
223
|
it('an optional slot is skipped for the literal the pattern wants next, when the rest then reads to the end', () => {
|
|
140
224
|
const p = pattern([lit('go'), group(slot('x', { optional: true })), lit('now')]);
|
|
141
225
|
expect(read(p, ['go', 'keyword'], ['now', 'keyword'])).toEqual({});
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* - A **greedy** slot (`RoleSpec.greedy`: a SQL condition, a column list) reads
|
|
8
8
|
* every token up to the next marker the pattern still owes, or the end, as
|
|
9
9
|
* one expression (`readGreedy`).
|
|
10
|
+
* - A slot with a **word list** (`RoleSpec.words`: behaviorspec's interaction,
|
|
11
|
+
* voice's thing to close) reads only one of its words, compared lower-cased
|
|
12
|
+
* by value or normalized form; any other word leaves the slot unread.
|
|
10
13
|
* - Any other slot reads one value (`readRoleValue`, the old matcher's reading):
|
|
11
14
|
* a selector, a quoted string, a number, a boolean, a duration, a URL, an
|
|
12
15
|
* identifier, or one of the multi-token forms it knows (`user.name`,
|
|
13
16
|
* `#dialog.showModal()`), checked against the slot's expected types.
|
|
14
17
|
* - A **required** slot reads nothing where the stream is at the marker the
|
|
15
18
|
* pattern wants next (the old matcher's guard against an SOV marker taken as
|
|
16
|
-
* a value).
|
|
19
|
+
* a value). An **optional** one reads nothing at any marker the rest of the
|
|
20
|
+
* pattern owes, through its optional groups (behaviorspec `when user types
|
|
21
|
+
* into #search`, whose typed text is optional and has no marker). Where a required slot finds nothing to read, and the optional,
|
|
17
22
|
* marker-less slot just before it read a value with nothing read since, that
|
|
18
23
|
* value is given back and the required slot reads again (the old matcher's
|
|
19
24
|
* backtrack: behaviorspec `when clicks`, where the optional actor took
|
|
@@ -89,6 +94,13 @@ export function readGreedy(
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/** The stock reader, for a matcher whose profiles it may consult (possessive words). */
|
|
97
|
+
/** Whether `token` is one of a slot's `words`, by value or normalized form, lower-cased. */
|
|
98
|
+
export function isListedWord(token: LanguageToken | null, words: readonly string[]): boolean {
|
|
99
|
+
if (!token) return false;
|
|
100
|
+
const forms = [token.value, token.normalized].filter((f): f is string => typeof f === 'string');
|
|
101
|
+
return forms.some(form => words.some(word => word.toLowerCase() === form.toLowerCase()));
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
export class StockValueReader implements ValueReader {
|
|
93
105
|
private profile: ReadingProfile | undefined;
|
|
94
106
|
/** The last optional, marker-less slot that read a value: what a failing required slot may take back. */
|
|
@@ -147,6 +159,13 @@ export class StockValueReader implements ValueReader {
|
|
|
147
159
|
const token = tokens.peek();
|
|
148
160
|
if (token && this.view.wouldMatch(next, token)) return false;
|
|
149
161
|
}
|
|
162
|
+
if (slot.words && !isListedWord(tokens.peek(), slot.words)) return slot.optional ?? false;
|
|
163
|
+
// Nor does an optional slot read a marker the rest of the pattern owes, through its optional
|
|
164
|
+
// groups (`when user types into #search`: `into` is the destination's, not the typed text).
|
|
165
|
+
if (slot.optional && !slot.greedy) {
|
|
166
|
+
const token = tokens.peek();
|
|
167
|
+
if (token && isStopMarker(token, greedyStopMarkers(this.view.patternRest()))) return true;
|
|
168
|
+
}
|
|
150
169
|
if (slot.greedy) {
|
|
151
170
|
const value = readGreedy(tokens, greedyStopMarkers(this.view.patternRest()));
|
|
152
171
|
if (value) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reading a domain value at a role slot: the value reading framework's
|
|
3
|
-
* matcher
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Reading a domain value at a role slot: the value reading framework's first
|
|
3
|
+
* matcher did, moved here for the stock reader on the matcher's skeleton
|
|
4
|
+
* (`stock-reader.ts`, domains rethink Phase 4.7). The first matcher was
|
|
5
|
+
* deleted in 5.0 (Phase 6).
|
|
6
6
|
*
|
|
7
7
|
* In order: an English noise word before a selector (`the`), then the
|
|
8
8
|
* multi-token forms (a possessive, `my value`; a method call,
|
|
@@ -22,8 +22,8 @@ import type {
|
|
|
22
22
|
LanguageToken,
|
|
23
23
|
} from '../types';
|
|
24
24
|
import { createSelector, createLiteral, createReference, createPropertyPath } from '../types';
|
|
25
|
-
import { isTypeCompatible } from '
|
|
26
|
-
import { getPossessiveReference } from '
|
|
25
|
+
import { isTypeCompatible } from './type-validation';
|
|
26
|
+
import { getPossessiveReference } from './possessive-keywords';
|
|
27
27
|
|
|
28
28
|
/** What value reading needs of a language profile: its code, and its possessive words. */
|
|
29
29
|
export interface ReadingProfile {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type Validation Tests
|
|
3
3
|
*
|
|
4
|
-
* Tests the type compatibility and validation utilities
|
|
4
|
+
* Tests the type compatibility and validation utilities the stock reader uses.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { describe, it, expect } from 'vitest';
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
isVariableRef,
|
|
18
18
|
isBuiltInReference,
|
|
19
19
|
} from './type-validation';
|
|
20
|
-
import type { SemanticValue } from '
|
|
20
|
+
import type { SemanticValue } from '../types';
|
|
21
21
|
|
|
22
22
|
describe('Type Validation', () => {
|
|
23
23
|
describe('isTypeCompatible', () => {
|
package/src/core/types.ts
CHANGED
|
@@ -174,6 +174,11 @@ export interface RolePatternToken {
|
|
|
174
174
|
readonly optional?: boolean;
|
|
175
175
|
readonly expectedTypes?: Array<ExpectedType>;
|
|
176
176
|
readonly greedy?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* The words the slot reads, from the schema's `RoleSpec.words` for the pattern's language (its
|
|
179
|
+
* `'*'` list and the language's own): the stock reader reads the slot only from one of them.
|
|
180
|
+
*/
|
|
181
|
+
readonly words?: readonly string[];
|
|
177
182
|
/**
|
|
178
183
|
* Mirrors the schema's `RoleSpec.valueShape`: the slot's surface form is
|
|
179
184
|
* unambiguous on its own. Carried onto the pattern token because the value
|
|
@@ -12,7 +12,7 @@ import { createDomainRenderer, createSchemaRenderer } from './renderer';
|
|
|
12
12
|
import type { SemanticNode } from '../core/types';
|
|
13
13
|
import type { CommandSchema } from '../schema';
|
|
14
14
|
import { defineCommand, defineRole } from '../schema';
|
|
15
|
-
import type { PatternGenLanguageProfile } from './
|
|
15
|
+
import type { PatternGenLanguageProfile } from './profile';
|
|
16
16
|
|
|
17
17
|
function makeNode(action: string, roles: Record<string, string>): SemanticNode {
|
|
18
18
|
const rolesMap = new Map<string, { type: 'expression'; raw: string }>();
|