kairos-lang 1.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +260 -0
- package/dist/ast.d.ts +142 -0
- package/dist/ast.js +1 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +309 -0
- package/dist/eval.d.ts +438 -0
- package/dist/eval.js +3198 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +64 -0
- package/dist/lexer.d.ts +33 -0
- package/dist/lexer.js +171 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.js +644 -0
- package/dist/tz.d.ts +57 -0
- package/dist/tz.js +216 -0
- package/package.json +63 -0
- package/stdlib/fiscal.kairos +12 -0
- package/stdlib/gregorian.kairos +36 -0
- package/stdlib/isoweek.kairos +16 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
// Kairos 構文解析器(spec §5.6 EBNF)
|
|
2
|
+
// 再帰下降。優先順位は EBNF の入れ子どおり:
|
|
3
|
+
// stream-expr = pipe { (|,&,\) pipe }(同一優先度・左結合)
|
|
4
|
+
// pipe = atom { |> stage }
|
|
5
|
+
// value-expr = ternary > or > and > not > comparison > additive > multiplicative > unary > postfix
|
|
6
|
+
import { lex, LexError } from "./lexer.js";
|
|
7
|
+
export class ParseError extends Error {
|
|
8
|
+
constructor(msg, tok) {
|
|
9
|
+
super(`構文エラー(${tok.line}:${tok.col}): ${msg}('${tok.text || tok.kind}' の位置)`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const GEN_WORDS = new Set(['grid', 'span', 'split', 'cycle']);
|
|
13
|
+
const MEMBER_KEYS = new Set(['calendar-system', 'calendar', 'axis', 'roll', 'granularity',
|
|
14
|
+
'tz', 'wkst', 'asof', 'source', 'epoch']);
|
|
15
|
+
const WORD_OPS = new Set(['and', 'or', 'not', 'mod', 'div', 'in', 'premise', 'with']);
|
|
16
|
+
export function parse(src) {
|
|
17
|
+
return new Parser(lex(src)).program();
|
|
18
|
+
}
|
|
19
|
+
/** covering-list 字面の単体パース(external の解決値 wire 用。ADR-46)——
|
|
20
|
+
* 端の含意・年略記・開端・区間リストの意味論をテーブルリテラルと完全共有するための入口 */
|
|
21
|
+
export function parseCoveringText(text) {
|
|
22
|
+
const p = new Parser(lex(text));
|
|
23
|
+
return p.coveringOnly();
|
|
24
|
+
}
|
|
25
|
+
class Parser {
|
|
26
|
+
i = 0;
|
|
27
|
+
toks;
|
|
28
|
+
constructor(toks) { this.toks = toks; }
|
|
29
|
+
peek(o = 0) { return this.toks[Math.min(this.i + o, this.toks.length - 1)]; }
|
|
30
|
+
next() { return this.toks[this.i++]; }
|
|
31
|
+
at(kind, text) {
|
|
32
|
+
const t = this.peek();
|
|
33
|
+
return t.kind === kind && (text === undefined || t.text === text);
|
|
34
|
+
}
|
|
35
|
+
atPunct(text) { return this.at('punct', text); }
|
|
36
|
+
atName(text) { return this.at('name', text); }
|
|
37
|
+
eat(kind, text) {
|
|
38
|
+
if (!this.at(kind, text))
|
|
39
|
+
throw new ParseError(`${text ?? kind} を期待`, this.peek());
|
|
40
|
+
return this.next();
|
|
41
|
+
}
|
|
42
|
+
skipNewlines() { while (this.at('newline'))
|
|
43
|
+
this.next(); }
|
|
44
|
+
/** newline の先が式の継続——段接続 |> または結合子 | & \——なら newline を飛ばす(F91・ADR-44。
|
|
45
|
+
* 文・前文メンバーは結合子で始まれないので一義: 行頭の結合子=前行の継続) */
|
|
46
|
+
skipContinuation() {
|
|
47
|
+
let j = this.i;
|
|
48
|
+
while (this.toks[j]?.kind === 'newline')
|
|
49
|
+
j++;
|
|
50
|
+
const t = this.toks[j];
|
|
51
|
+
if (t?.kind === 'punct' && (t.text === '|>' || t.text === '|' || t.text === '&' || t.text === '\\')) {
|
|
52
|
+
this.i = j;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
program() {
|
|
56
|
+
const statements = [];
|
|
57
|
+
this.skipNewlines();
|
|
58
|
+
while (!this.at('eof')) {
|
|
59
|
+
statements.push(this.statement());
|
|
60
|
+
this.skipNewlines();
|
|
61
|
+
while (this.atPunct(';')) {
|
|
62
|
+
this.next();
|
|
63
|
+
this.skipNewlines();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { statements };
|
|
67
|
+
}
|
|
68
|
+
statement() {
|
|
69
|
+
if (this.atName('premise')) {
|
|
70
|
+
// premise-def か完全形前文か: `premise 名前 …` は定義、`premise {` は前文
|
|
71
|
+
if (this.peek(1).kind === 'name')
|
|
72
|
+
return this.premiseDef();
|
|
73
|
+
this.next();
|
|
74
|
+
const block = this.premiseBlock();
|
|
75
|
+
return { t: 'preamble', form: 'inline', members: block.members };
|
|
76
|
+
}
|
|
77
|
+
if (this.atPunct('@'))
|
|
78
|
+
return this.preamble();
|
|
79
|
+
// binding か stream-expr か: NAME [(params)] = … を先読み
|
|
80
|
+
if (this.at('name')) {
|
|
81
|
+
const save = this.i;
|
|
82
|
+
const name = this.next().text;
|
|
83
|
+
let params = [];
|
|
84
|
+
if (this.atPunct('('))
|
|
85
|
+
params = this.tryParams();
|
|
86
|
+
if (params !== null && this.atPunct('=')) {
|
|
87
|
+
this.next();
|
|
88
|
+
const rhs = this.expression();
|
|
89
|
+
const covering = this.tryBindingCovering(); // 束縛後置=明示の被覆主張(ADR-37 判断 5)
|
|
90
|
+
return { t: 'binding', name, params, rhs, covering };
|
|
91
|
+
}
|
|
92
|
+
this.i = save;
|
|
93
|
+
}
|
|
94
|
+
return { t: 'streamExpr', expr: this.expression() };
|
|
95
|
+
}
|
|
96
|
+
/** 束縛の仮引数 (a, b, on: p)。パターン外なら null(呼び出し式だった) */
|
|
97
|
+
tryParams() {
|
|
98
|
+
const save = this.i;
|
|
99
|
+
this.eat('punct', '(');
|
|
100
|
+
const params = [];
|
|
101
|
+
if (!this.atPunct(')')) {
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (!this.at('name')) {
|
|
104
|
+
this.i = save;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const first = this.next().text;
|
|
108
|
+
if (this.atPunct(':')) { // named-param: on: p
|
|
109
|
+
this.next();
|
|
110
|
+
if (!this.at('name')) {
|
|
111
|
+
this.i = save;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
params.push({ key: first, name: this.next().text });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
params.push({ name: first });
|
|
118
|
+
}
|
|
119
|
+
if (this.atPunct(',')) {
|
|
120
|
+
this.next();
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!this.atPunct(')')) {
|
|
127
|
+
this.i = save;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
this.next();
|
|
131
|
+
return params;
|
|
132
|
+
}
|
|
133
|
+
premiseDef() {
|
|
134
|
+
this.eat('name', 'premise');
|
|
135
|
+
const name = this.eat('name').text;
|
|
136
|
+
if (this.atPunct('{'))
|
|
137
|
+
return { t: 'premiseDef', name, block: this.premiseBlock() };
|
|
138
|
+
this.eat('punct', '=');
|
|
139
|
+
const base = this.eat('name').text;
|
|
140
|
+
let withBlock;
|
|
141
|
+
const stages = [];
|
|
142
|
+
if (this.atName('with')) {
|
|
143
|
+
this.next();
|
|
144
|
+
withBlock = this.premiseBlock();
|
|
145
|
+
}
|
|
146
|
+
this.skipContinuation();
|
|
147
|
+
while (this.atPunct('|>')) {
|
|
148
|
+
this.next();
|
|
149
|
+
stages.push(this.stage());
|
|
150
|
+
this.skipContinuation();
|
|
151
|
+
}
|
|
152
|
+
return { t: 'premiseDef', name, expr: { base, withBlock, stages } };
|
|
153
|
+
}
|
|
154
|
+
premiseBlock() {
|
|
155
|
+
this.eat('punct', '{');
|
|
156
|
+
const members = [];
|
|
157
|
+
const bindings = [];
|
|
158
|
+
this.skipNewlines();
|
|
159
|
+
while (!this.atPunct('}')) {
|
|
160
|
+
if (this.at('name') && MEMBER_KEYS.has(this.peek().text) && this.peek(1).text === ':') {
|
|
161
|
+
members.push(this.member());
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const name = this.eat('name').text;
|
|
165
|
+
let params = [];
|
|
166
|
+
if (this.atPunct('(')) {
|
|
167
|
+
const p = this.tryParams();
|
|
168
|
+
if (p === null)
|
|
169
|
+
throw new ParseError('束縛の仮引数を期待', this.peek());
|
|
170
|
+
params = p;
|
|
171
|
+
}
|
|
172
|
+
this.eat('punct', '=');
|
|
173
|
+
const rhs = this.expression();
|
|
174
|
+
const covering = this.tryBindingCovering(); // 束縛後置=明示の被覆主張(ADR-37 判断 5)
|
|
175
|
+
bindings.push({ name, params, rhs, covering });
|
|
176
|
+
}
|
|
177
|
+
this.skipNewlines();
|
|
178
|
+
while (this.atPunct(';')) {
|
|
179
|
+
this.next();
|
|
180
|
+
this.skipNewlines();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.eat('punct', '}');
|
|
184
|
+
return { members, bindings };
|
|
185
|
+
}
|
|
186
|
+
member() {
|
|
187
|
+
const key = this.eat('name').text;
|
|
188
|
+
this.eat('punct', ':');
|
|
189
|
+
return { key, value: this.valueAtomForMember() };
|
|
190
|
+
}
|
|
191
|
+
/** メンバー値は単純式(名前・数値・日付・文字列)に限る */
|
|
192
|
+
valueAtomForMember() {
|
|
193
|
+
const t = this.peek();
|
|
194
|
+
if (t.kind === 'date') {
|
|
195
|
+
this.next();
|
|
196
|
+
return { t: 'date', v: t.date };
|
|
197
|
+
}
|
|
198
|
+
if (t.kind === 'number') {
|
|
199
|
+
this.next();
|
|
200
|
+
return { t: 'num', v: t.num };
|
|
201
|
+
}
|
|
202
|
+
if (t.kind === 'string') {
|
|
203
|
+
this.next();
|
|
204
|
+
return { t: 'str', v: t.text };
|
|
205
|
+
}
|
|
206
|
+
if (t.kind === 'name') {
|
|
207
|
+
this.next();
|
|
208
|
+
return { t: 'name', name: t.text };
|
|
209
|
+
}
|
|
210
|
+
throw new ParseError('前文メンバーの値を期待', t);
|
|
211
|
+
}
|
|
212
|
+
preamble() {
|
|
213
|
+
this.eat('punct', '@');
|
|
214
|
+
const name = this.eat('name').text;
|
|
215
|
+
const members = [];
|
|
216
|
+
// 後置畳み込み: @JP axis: bizDay …(行末まで)
|
|
217
|
+
while (this.at('name') && MEMBER_KEYS.has(this.peek().text) && this.peek(1).text === ':') {
|
|
218
|
+
members.push(this.member());
|
|
219
|
+
}
|
|
220
|
+
if (this.atPunct('{')) {
|
|
221
|
+
// ブロック形: @名前 { 文の並び }
|
|
222
|
+
this.next();
|
|
223
|
+
const block = [];
|
|
224
|
+
this.skipNewlines();
|
|
225
|
+
while (!this.atPunct('}')) {
|
|
226
|
+
block.push(this.statement());
|
|
227
|
+
this.skipNewlines();
|
|
228
|
+
}
|
|
229
|
+
this.eat('punct', '}');
|
|
230
|
+
return { t: 'preamble', form: 'light', name, members, block };
|
|
231
|
+
}
|
|
232
|
+
return { t: 'preamble', form: 'light', name, members };
|
|
233
|
+
}
|
|
234
|
+
// ---- 式 ----
|
|
235
|
+
expression() {
|
|
236
|
+
return this.combineExpr();
|
|
237
|
+
}
|
|
238
|
+
/** 結合子 | & \(同一優先度・左結合) */
|
|
239
|
+
combineExpr() {
|
|
240
|
+
let l = this.pipeExpr();
|
|
241
|
+
for (;;) {
|
|
242
|
+
this.skipContinuation();
|
|
243
|
+
if (this.atPunct('|') || this.atPunct('&') || this.atPunct('\\')) {
|
|
244
|
+
const op = this.next().text;
|
|
245
|
+
this.skipNewlines(); // 行末結合子=次行へ継続(F91・ADR-44。|> の行末継続と同じ扱い)
|
|
246
|
+
const r = this.pipeExpr();
|
|
247
|
+
l = { t: 'combine', op, l, r };
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
return l;
|
|
253
|
+
}
|
|
254
|
+
pipeExpr() {
|
|
255
|
+
let head = this.genOrValue();
|
|
256
|
+
const stages = [];
|
|
257
|
+
this.skipContinuation();
|
|
258
|
+
while (this.atPunct('|>')) {
|
|
259
|
+
this.next();
|
|
260
|
+
this.skipNewlines();
|
|
261
|
+
stages.push(this.stage());
|
|
262
|
+
this.skipContinuation();
|
|
263
|
+
}
|
|
264
|
+
return stages.length ? { t: 'pipe', head, stages } : head;
|
|
265
|
+
}
|
|
266
|
+
stage() {
|
|
267
|
+
let name = this.eat('name').text;
|
|
268
|
+
let ns;
|
|
269
|
+
if (this.atPunct('.')) {
|
|
270
|
+
this.next();
|
|
271
|
+
ns = name;
|
|
272
|
+
name = this.eat('name').text;
|
|
273
|
+
}
|
|
274
|
+
let args = [];
|
|
275
|
+
if (this.atPunct('('))
|
|
276
|
+
args = this.args();
|
|
277
|
+
return { name, ns, args };
|
|
278
|
+
}
|
|
279
|
+
args() {
|
|
280
|
+
this.eat('punct', '(');
|
|
281
|
+
const args = [];
|
|
282
|
+
const seen = new Set(); // named-arg の二重指定は黙殺せず静的エラー(ADR-47 で顕在化した穴)
|
|
283
|
+
if (!this.atPunct(')')) {
|
|
284
|
+
for (;;) {
|
|
285
|
+
if (this.at('name') && this.peek(1).text === ':') {
|
|
286
|
+
const name = this.next().text;
|
|
287
|
+
if (seen.has(name))
|
|
288
|
+
throw new ParseError(`名前付き引数 ${name}: の二重指定`, this.peek());
|
|
289
|
+
seen.add(name);
|
|
290
|
+
this.next(); // ':'
|
|
291
|
+
// labels: cycle […] anchor: 実日(窓列への周期ラベル・ADR-47)——cycle は core 語で
|
|
292
|
+
// 再束縛できないため、labels: 直後の cycle は常に cycle 形
|
|
293
|
+
if (name === 'labels' && this.atName('cycle')) {
|
|
294
|
+
args.push({ name, value: this.cycleLabels() });
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
args.push({ name, value: this.expression() });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
args.push({ value: this.expression() });
|
|
302
|
+
}
|
|
303
|
+
if (this.atPunct(',')) {
|
|
304
|
+
this.next();
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this.eat('punct', ')');
|
|
311
|
+
return args;
|
|
312
|
+
}
|
|
313
|
+
/** labels: の cycle 形(ADR-47): cycle (リスト|リスト束縛名) anchor: 実日。anchor は形の一部
|
|
314
|
+
* (カンマで切ると segmentBy の未知引数に化ける——専用文言で導く) */
|
|
315
|
+
cycleLabels() {
|
|
316
|
+
this.eat('name', 'cycle');
|
|
317
|
+
let list;
|
|
318
|
+
if (this.atPunct('['))
|
|
319
|
+
list = this.listLiteral(false);
|
|
320
|
+
else if (this.at('name'))
|
|
321
|
+
list = { t: 'name', name: this.next().text };
|
|
322
|
+
else
|
|
323
|
+
throw new ParseError('labels: cycle はリスト(リテラルまたはリスト束縛名)を取る', this.peek());
|
|
324
|
+
if (this.atPunct(',') && this.peek(1).kind === 'name' && this.peek(1).text === 'anchor') {
|
|
325
|
+
throw new ParseError('labels: cycle の anchor: はカンマなしで続ける'
|
|
326
|
+
+ '(cycle 形の一部——labels: cycle […] anchor: 実日)', this.peek());
|
|
327
|
+
}
|
|
328
|
+
if (!(this.atName('anchor') && this.peek(1).text === ':')) {
|
|
329
|
+
throw new ParseError('labels: cycle は anchor: が必須(位相を留める実日)', this.peek());
|
|
330
|
+
}
|
|
331
|
+
this.next();
|
|
332
|
+
this.next();
|
|
333
|
+
return { t: 'cycleLabels', list, anchor: this.ternary() };
|
|
334
|
+
}
|
|
335
|
+
/** gen-expr(`day span f phase: 0`)と値式を判別。並置は窓生成語 4 語のみ(EBNF・F46) */
|
|
336
|
+
genOrValue() {
|
|
337
|
+
const e = this.ternary();
|
|
338
|
+
if (e.t === 'name' && this.at('name') && !WORD_OPS.has(this.peek().text)) {
|
|
339
|
+
const word = this.peek().text;
|
|
340
|
+
if (GEN_WORDS.has(word)) {
|
|
341
|
+
this.next();
|
|
342
|
+
const arg = this.genArg();
|
|
343
|
+
const named = this.namedArgsTrail();
|
|
344
|
+
return { t: 'gen', operand: e, word: word, arg, named };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return e;
|
|
348
|
+
}
|
|
349
|
+
genArg() {
|
|
350
|
+
const t = this.peek();
|
|
351
|
+
if (t.kind === 'width') {
|
|
352
|
+
this.next();
|
|
353
|
+
return { t: 'width', v: t.width };
|
|
354
|
+
}
|
|
355
|
+
if (this.atPunct('(')) {
|
|
356
|
+
// (ラムダ) または括弧式
|
|
357
|
+
const save = this.i;
|
|
358
|
+
this.next();
|
|
359
|
+
const inner = this.expression();
|
|
360
|
+
this.eat('punct', ')');
|
|
361
|
+
return inner;
|
|
362
|
+
}
|
|
363
|
+
if (this.atPunct('['))
|
|
364
|
+
return this.listLiteral();
|
|
365
|
+
if (t.kind === 'name') {
|
|
366
|
+
// ラムダ `n => …` か裸名
|
|
367
|
+
if (this.peek(1).text === '=>')
|
|
368
|
+
return this.ternary();
|
|
369
|
+
this.next();
|
|
370
|
+
return { t: 'name', name: t.text };
|
|
371
|
+
}
|
|
372
|
+
if (this.atPunct('_'))
|
|
373
|
+
return this.ternary();
|
|
374
|
+
throw new ParseError('窓生成語の引数を期待', t);
|
|
375
|
+
}
|
|
376
|
+
namedArgsTrail() {
|
|
377
|
+
const named = {};
|
|
378
|
+
while (this.at('name') && this.peek(1).text === ':' && !MEMBER_KEYS.has(this.peek().text)) {
|
|
379
|
+
const key = this.next().text;
|
|
380
|
+
this.next();
|
|
381
|
+
named[key] = this.ternary();
|
|
382
|
+
}
|
|
383
|
+
return named;
|
|
384
|
+
}
|
|
385
|
+
// ---- 値式(EBNF の入れ子どおり) ----
|
|
386
|
+
ternary() {
|
|
387
|
+
const c = this.orExpr();
|
|
388
|
+
if (this.atPunct('?')) {
|
|
389
|
+
this.next();
|
|
390
|
+
const a = this.ternary();
|
|
391
|
+
this.eat('punct', ':');
|
|
392
|
+
const b = this.ternary();
|
|
393
|
+
return { t: 'ternary', c, a, b };
|
|
394
|
+
}
|
|
395
|
+
return c;
|
|
396
|
+
}
|
|
397
|
+
orExpr() {
|
|
398
|
+
let l = this.andExpr();
|
|
399
|
+
while (this.atName('or')) {
|
|
400
|
+
this.next();
|
|
401
|
+
l = { t: 'bin', op: 'or', l, r: this.andExpr() };
|
|
402
|
+
}
|
|
403
|
+
return l;
|
|
404
|
+
}
|
|
405
|
+
andExpr() {
|
|
406
|
+
let l = this.notExpr();
|
|
407
|
+
while (this.atName('and')) {
|
|
408
|
+
this.next();
|
|
409
|
+
l = { t: 'bin', op: 'and', l, r: this.notExpr() };
|
|
410
|
+
}
|
|
411
|
+
return l;
|
|
412
|
+
}
|
|
413
|
+
notExpr() {
|
|
414
|
+
if (this.atName('not')) {
|
|
415
|
+
this.next();
|
|
416
|
+
return { t: 'not', e: this.notExpr() };
|
|
417
|
+
}
|
|
418
|
+
return this.comparison();
|
|
419
|
+
}
|
|
420
|
+
comparison() {
|
|
421
|
+
const l = this.additive();
|
|
422
|
+
const t = this.peek();
|
|
423
|
+
if ((t.kind === 'punct' && ['==', '!=', '<', '<=', '>', '>='].includes(t.text)) ||
|
|
424
|
+
(t.kind === 'name' && t.text === 'in')) {
|
|
425
|
+
this.next();
|
|
426
|
+
return { t: 'bin', op: t.text, l, r: this.additive() };
|
|
427
|
+
}
|
|
428
|
+
return l;
|
|
429
|
+
}
|
|
430
|
+
additive() {
|
|
431
|
+
let l = this.multiplicative();
|
|
432
|
+
while (this.atPunct('+') || this.atPunct('-')) {
|
|
433
|
+
const op = this.next().text;
|
|
434
|
+
l = { t: 'bin', op, l, r: this.multiplicative() };
|
|
435
|
+
}
|
|
436
|
+
return l;
|
|
437
|
+
}
|
|
438
|
+
multiplicative() {
|
|
439
|
+
let l = this.unary();
|
|
440
|
+
while (this.atPunct('*') || this.atPunct('/') || this.atName('mod') || this.atName('div')) {
|
|
441
|
+
const op = this.next().text;
|
|
442
|
+
l = { t: 'bin', op, l, r: this.unary() };
|
|
443
|
+
}
|
|
444
|
+
return l;
|
|
445
|
+
}
|
|
446
|
+
unary() {
|
|
447
|
+
if (this.atPunct('-')) {
|
|
448
|
+
this.next();
|
|
449
|
+
return { t: 'neg', e: this.unary() };
|
|
450
|
+
}
|
|
451
|
+
if (this.atPunct('+')) {
|
|
452
|
+
this.next();
|
|
453
|
+
return this.unary();
|
|
454
|
+
}
|
|
455
|
+
return this.postfix();
|
|
456
|
+
}
|
|
457
|
+
postfix() {
|
|
458
|
+
let e = this.atom();
|
|
459
|
+
for (;;) {
|
|
460
|
+
if (this.atPunct('[')) {
|
|
461
|
+
this.next();
|
|
462
|
+
const index = this.expression();
|
|
463
|
+
this.eat('punct', ']');
|
|
464
|
+
e = { t: 'index', target: e, index };
|
|
465
|
+
}
|
|
466
|
+
else if (this.atPunct('(')) {
|
|
467
|
+
e = { t: 'call', callee: e, args: this.args() };
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
return e;
|
|
473
|
+
}
|
|
474
|
+
atom() {
|
|
475
|
+
const t = this.peek();
|
|
476
|
+
// ラムダ: name => / _ => / (params) =>
|
|
477
|
+
if ((t.kind === 'name' || t.text === '_') && this.peek(1).text === '=>') {
|
|
478
|
+
const p = this.next().text;
|
|
479
|
+
this.next();
|
|
480
|
+
const body = this.expression();
|
|
481
|
+
return { t: 'lambda', params: [p], body };
|
|
482
|
+
}
|
|
483
|
+
if (t.text === '(') {
|
|
484
|
+
// (a, b) => … の多引数ラムダを先読み
|
|
485
|
+
const save = this.i;
|
|
486
|
+
const params = this.tryParams();
|
|
487
|
+
if (params !== null && this.atPunct('=>')) {
|
|
488
|
+
this.next();
|
|
489
|
+
return { t: 'lambda', params: params.map(p => p.name), body: this.expression() };
|
|
490
|
+
}
|
|
491
|
+
this.i = save;
|
|
492
|
+
this.next();
|
|
493
|
+
const inner = this.expression();
|
|
494
|
+
this.eat('punct', ')');
|
|
495
|
+
return inner;
|
|
496
|
+
}
|
|
497
|
+
if (t.kind === 'number') {
|
|
498
|
+
this.next();
|
|
499
|
+
return { t: 'num', v: t.num };
|
|
500
|
+
}
|
|
501
|
+
if (t.kind === 'date') {
|
|
502
|
+
this.next();
|
|
503
|
+
return { t: 'date', v: t.date };
|
|
504
|
+
}
|
|
505
|
+
if (t.kind === 'time') {
|
|
506
|
+
this.next();
|
|
507
|
+
return { t: 'time', tod: t.tod };
|
|
508
|
+
}
|
|
509
|
+
if (t.kind === 'width') {
|
|
510
|
+
this.next();
|
|
511
|
+
return { t: 'width', v: t.width };
|
|
512
|
+
}
|
|
513
|
+
if (t.kind === 'string') {
|
|
514
|
+
this.next();
|
|
515
|
+
return { t: 'str', v: t.text };
|
|
516
|
+
}
|
|
517
|
+
if (t.text === '[')
|
|
518
|
+
return this.listLiteral();
|
|
519
|
+
if (t.text === '_') {
|
|
520
|
+
this.next();
|
|
521
|
+
return { t: 'name', name: '_' };
|
|
522
|
+
}
|
|
523
|
+
if (t.kind === 'name') {
|
|
524
|
+
this.next();
|
|
525
|
+
let e = { t: 'name', name: t.text };
|
|
526
|
+
if (this.atPunct('.') && this.peek(1).kind === 'name' && this.peek(2).text !== '.') {
|
|
527
|
+
this.next();
|
|
528
|
+
e = { t: 'qualified', ns: t.text, name: this.eat('name').text };
|
|
529
|
+
}
|
|
530
|
+
return e;
|
|
531
|
+
}
|
|
532
|
+
throw new ParseError('式を期待', t);
|
|
533
|
+
}
|
|
534
|
+
/** リスト/テーブルリテラル(covering: / labels: 後置つき。§3.8・ADR-26/30) */
|
|
535
|
+
listLiteral(allowPostfix = true) {
|
|
536
|
+
this.eat('punct', '[');
|
|
537
|
+
const elems = [];
|
|
538
|
+
if (!this.atPunct(']')) {
|
|
539
|
+
for (;;) {
|
|
540
|
+
const e = this.ternary();
|
|
541
|
+
if (this.atPunct('..')) {
|
|
542
|
+
this.next();
|
|
543
|
+
elems.push({ t: 'range', a: e, b: this.ternary() });
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
elems.push(e);
|
|
547
|
+
}
|
|
548
|
+
if (this.atPunct(',')) {
|
|
549
|
+
this.next();
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.eat('punct', ']');
|
|
556
|
+
if (!allowPostfix)
|
|
557
|
+
return { t: 'list', elems };
|
|
558
|
+
// 後置は順序自由(F104——labels: の値リストが後続の covering: を吸って黙殺する穴の封じ。
|
|
559
|
+
// labels: の値は後置なしでパースし、covering:/labels: は外側でどの順でも受ける。二重指定はエラー)
|
|
560
|
+
let covering;
|
|
561
|
+
let labels;
|
|
562
|
+
for (;;) {
|
|
563
|
+
this.skipToPostfixKeyword();
|
|
564
|
+
if (this.atName('covering') && this.peek(1).text === ':') {
|
|
565
|
+
if (covering)
|
|
566
|
+
throw new ParseError('covering: の二重指定', this.peek());
|
|
567
|
+
this.next();
|
|
568
|
+
this.next();
|
|
569
|
+
covering = this.coveringList();
|
|
570
|
+
}
|
|
571
|
+
else if (this.atName('labels') && this.peek(1).text === ':') {
|
|
572
|
+
if (labels)
|
|
573
|
+
throw new ParseError('labels: の二重指定', this.peek());
|
|
574
|
+
this.next();
|
|
575
|
+
this.next();
|
|
576
|
+
labels = this.listLiteral(false);
|
|
577
|
+
}
|
|
578
|
+
else
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
return { t: 'list', elems, covering, labels };
|
|
582
|
+
}
|
|
583
|
+
/** covering:/labels: は改行を挟んだ継続を許す(|> の継続と同じ扱い) */
|
|
584
|
+
skipToPostfixKeyword() {
|
|
585
|
+
let j = this.i;
|
|
586
|
+
while (this.toks[j]?.kind === 'newline')
|
|
587
|
+
j++;
|
|
588
|
+
const t = this.toks[j];
|
|
589
|
+
if (t?.kind === 'name' && (t.text === 'covering' || t.text === 'labels')
|
|
590
|
+
&& this.toks[j + 1]?.text === ':')
|
|
591
|
+
this.i = j;
|
|
592
|
+
}
|
|
593
|
+
/** 束縛後置の covering:(明示の被覆主張。rhs が裸のテーブルリテラルなら listLiteral が先に食う) */
|
|
594
|
+
tryBindingCovering() {
|
|
595
|
+
this.skipToPostfixKeyword();
|
|
596
|
+
if (this.atName('covering') && this.peek(1).text === ':') {
|
|
597
|
+
this.next();
|
|
598
|
+
this.next();
|
|
599
|
+
return this.coveringList();
|
|
600
|
+
}
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
/** covering-list 単体のパース(parseCoveringText の実体。末尾は EOF を要求) */
|
|
604
|
+
coveringOnly() {
|
|
605
|
+
this.skipNewlines();
|
|
606
|
+
const list = this.coveringList();
|
|
607
|
+
this.skipNewlines();
|
|
608
|
+
if (!this.at('eof'))
|
|
609
|
+
throw new ParseError('covering-list の末尾に余分な字句', this.peek());
|
|
610
|
+
return list;
|
|
611
|
+
}
|
|
612
|
+
/** covering-list = covering-range { "," covering-range }(§5.6・ADR-37 判断 9) */
|
|
613
|
+
coveringList() {
|
|
614
|
+
const list = [];
|
|
615
|
+
for (;;) {
|
|
616
|
+
list.push(this.coveringRange());
|
|
617
|
+
// 区間リストの継続: "," の先が covering-range の開始(数値・日付・..)のときだけ食う
|
|
618
|
+
// (引数位置のテーブルリテラルでは "," は外側の引数区切り)
|
|
619
|
+
if (this.atPunct(',')) {
|
|
620
|
+
const t = this.peek(1);
|
|
621
|
+
if (t.kind === 'number' || t.kind === 'date' || (t.kind === 'punct' && t.text === '..')) {
|
|
622
|
+
this.next();
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
return list;
|
|
629
|
+
}
|
|
630
|
+
/** covering-range = [端] ".." [端]。端の省略=開端(完結主張・ADR-37)。
|
|
631
|
+
* 後端は字句で判定(covering-edge = 年 | 日付)——`covering: 2021..` の直後の labels: 等を食わない */
|
|
632
|
+
coveringRange() {
|
|
633
|
+
let a = null;
|
|
634
|
+
if (!this.atPunct('..'))
|
|
635
|
+
a = this.ternary();
|
|
636
|
+
this.eat('punct', '..');
|
|
637
|
+
let b = null;
|
|
638
|
+
const t = this.peek();
|
|
639
|
+
if (t.kind === 'number' || t.kind === 'date')
|
|
640
|
+
b = this.ternary();
|
|
641
|
+
return { a, b };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
export { LexError };
|
package/dist/tz.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CivilFields {
|
|
2
|
+
y: number;
|
|
3
|
+
mo: number;
|
|
4
|
+
d: number;
|
|
5
|
+
h: number;
|
|
6
|
+
mi: number;
|
|
7
|
+
s: number;
|
|
8
|
+
ms: number;
|
|
9
|
+
}
|
|
10
|
+
export type AnchorResult = {
|
|
11
|
+
kind: 'unique';
|
|
12
|
+
ms: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'gap';
|
|
15
|
+
ms: number;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'overlap';
|
|
18
|
+
ms: number;
|
|
19
|
+
alt: number;
|
|
20
|
+
};
|
|
21
|
+
export declare class Tz {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** 区間 [t[i], t[i+1]) のオフセット off[i](分)。t[0] = RANGE_LO */
|
|
24
|
+
private t;
|
|
25
|
+
private off;
|
|
26
|
+
/** 市民日開始の実体化キャッシュ(1970-01-01 の市民日が添字 0) */
|
|
27
|
+
private days;
|
|
28
|
+
private daysHi;
|
|
29
|
+
constructor(name: string);
|
|
30
|
+
/** 瞬間のオフセット(分) */
|
|
31
|
+
offsetAt(ms: number): number;
|
|
32
|
+
/** 瞬間の市民フィールド */
|
|
33
|
+
localFields(ms: number): CivilFields;
|
|
34
|
+
/** 市民フィールド(local-as-UTC ms)の錨打ち。DST の隙間・重複を検出する(ADR-33) */
|
|
35
|
+
anchor(localAsUtc: number): AnchorResult;
|
|
36
|
+
/** 市民日の開始=「その日付になる最初の瞬間」(ADR-33 判断 4。真夜中が隙間なら遷移点) */
|
|
37
|
+
civilDayStart(y: number, mo: number, d: number): number;
|
|
38
|
+
/** 市民日開始 ms を n 市民日ぶん歩進(フィールド演算=壁時計保存) */
|
|
39
|
+
addCivilDays(dayStartMs: number, n: number): number;
|
|
40
|
+
/** ms の属する市民日の開始 */
|
|
41
|
+
floorToDay(ms: number): number;
|
|
42
|
+
/** 瞬間の壁時計の日内ラベル(h:m:s.ms の ms 換算)——日開始からの経過ではなくラベル読み。
|
|
43
|
+
* 時刻付き anchor/from: の窓境界の正(ADR-31 改訂 2・F87): 切替日の anchor でも
|
|
44
|
+
* atTimeOfDay と往復して「各市民日でその壁時計を最初に読む瞬間」が立つ */
|
|
45
|
+
timeOfDayLabel(ms: number): number;
|
|
46
|
+
/** 市民日 D(開始 ms)で壁時計 (h,m,s) = todMs を最初に読む瞬間(ADR-31 改訂 2):
|
|
47
|
+
* 隙間(存在しない時刻)なら隙間明けの最初の瞬間・重複(二度ある時刻)なら最初の出現 */
|
|
48
|
+
atTimeOfDay(dayStartMs: number, todMs: number): number;
|
|
49
|
+
/** [1970-01-01 の市民日, hi) の市民日開始列(添字 0 =紀元の市民日。キャッシュ・追記拡張) */
|
|
50
|
+
dayStarts(hi: number): number[];
|
|
51
|
+
/** 市民日開始の紀元(1970-01-01 の市民日)からの序数。dayStarts の添字 */
|
|
52
|
+
dayIndex(dayStartMs: number): number;
|
|
53
|
+
/** 表示形(YYYY-MM-DD[Thh:mm[:ss]])——この tz の市民ラベル */
|
|
54
|
+
format(ms: number): string;
|
|
55
|
+
}
|
|
56
|
+
/** tz 名から Tz を得る(プロセス内キャッシュ)。不正な名前は静的エラー */
|
|
57
|
+
export declare function getTz(name: string): Tz;
|