aontu 0.68.0 → 0.69.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.
Files changed (74) hide show
  1. package/dist/alias.d.ts +12 -1
  2. package/dist/alias.js +176 -6
  3. package/dist/alias.js.map +1 -1
  4. package/dist/aliasname.d.ts +19 -0
  5. package/dist/aliasname.js +65 -0
  6. package/dist/aliasname.js.map +1 -0
  7. package/dist/aontu.d.ts +1 -1
  8. package/dist/aontu.js +21 -8
  9. package/dist/aontu.js.map +1 -1
  10. package/dist/aontumodel.js +1 -1
  11. package/dist/aontumodel.js.map +1 -1
  12. package/dist/ctx.d.ts +1 -0
  13. package/dist/ctx.js +1 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/err.js +18 -1
  16. package/dist/err.js.map +1 -1
  17. package/dist/format.js +14 -0
  18. package/dist/format.js.map +1 -1
  19. package/dist/hints.js +36 -7
  20. package/dist/hints.js.map +1 -1
  21. package/dist/lang.js +289 -35
  22. package/dist/lang.js.map +1 -1
  23. package/dist/lsp.d.ts +10 -4
  24. package/dist/lsp.js +116 -7
  25. package/dist/lsp.js.map +1 -1
  26. package/dist/site.d.ts +9 -0
  27. package/dist/site.js +2 -1
  28. package/dist/site.js.map +1 -1
  29. package/dist/subsume.js +6 -1
  30. package/dist/subsume.js.map +1 -1
  31. package/dist/val/BagVal.d.ts +1 -0
  32. package/dist/val/BagVal.js +3 -0
  33. package/dist/val/BagVal.js.map +1 -1
  34. package/dist/val/EmitFuncVal.js +2 -1
  35. package/dist/val/EmitFuncVal.js.map +1 -1
  36. package/dist/val/FilterFuncVal.js +1 -1
  37. package/dist/val/FilterFuncVal.js.map +1 -1
  38. package/dist/val/FuncBaseVal.d.ts +2 -1
  39. package/dist/val/FuncBaseVal.js +25 -1
  40. package/dist/val/FuncBaseVal.js.map +1 -1
  41. package/dist/val/MapVal.js +10 -4
  42. package/dist/val/MapVal.js.map +1 -1
  43. package/dist/val/MatchFuncVal.js +2 -1
  44. package/dist/val/MatchFuncVal.js.map +1 -1
  45. package/dist/val/NilVal.js +3 -0
  46. package/dist/val/NilVal.js.map +1 -1
  47. package/dist/val/RecurseVal.d.ts +1 -0
  48. package/dist/val/RecurseVal.js +7 -3
  49. package/dist/val/RecurseVal.js.map +1 -1
  50. package/dist/val/RefVal.d.ts +1 -0
  51. package/dist/val/RefVal.js +29 -12
  52. package/dist/val/RefVal.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/alias.ts +214 -6
  55. package/src/aliasname.ts +95 -0
  56. package/src/aontu.ts +23 -9
  57. package/src/aontumodel.ts +1 -1
  58. package/src/ctx.ts +4 -1
  59. package/src/err.ts +19 -1
  60. package/src/format.ts +14 -0
  61. package/src/hints.ts +40 -7
  62. package/src/lang.ts +334 -30
  63. package/src/lsp.ts +136 -6
  64. package/src/site.ts +15 -1
  65. package/src/subsume.ts +9 -2
  66. package/src/val/BagVal.ts +4 -0
  67. package/src/val/EmitFuncVal.ts +3 -2
  68. package/src/val/FilterFuncVal.ts +2 -2
  69. package/src/val/FuncBaseVal.ts +30 -1
  70. package/src/val/MapVal.ts +10 -4
  71. package/src/val/MatchFuncVal.ts +3 -2
  72. package/src/val/NilVal.ts +3 -0
  73. package/src/val/RecurseVal.ts +8 -3
  74. package/src/val/RefVal.ts +31 -12
package/src/lang.ts CHANGED
@@ -190,12 +190,34 @@ const CC_d = 100
190
190
  const CC_D = 68
191
191
 
192
192
  const CC_PCT = 37
193
- // Hyphen separates segments; not leading or trailing, as `-` prefixes negation.
194
- const ALIAS_RE = /^%[A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)*/
193
+ import type { AliasBind } from './aliasname'
194
+ import {
195
+ ALIAS_RE,
196
+ ALIAS_SET,
197
+ ALIAS_SHORTHAND_RE,
198
+ EXPORT_DECL_NAME,
199
+ RESERVED_KEY_PREFIX,
200
+ exportHoldKey,
201
+ aliasBareName,
202
+ aliasScopedKey,
203
+ aliasSetItems,
204
+ } from './aliasname'
195
205
 
196
206
  const CC_EQ = 61
197
207
  const CC_SP = 32
198
208
  const CC_TAB = 9
209
+ const CC_OB = 123
210
+ const WS_RE = /\s/
211
+
212
+ let SCOPE_SEQ = 0
213
+ const MERGE_KEY = RESERVED_KEY_PREFIX + 'merge'
214
+ const ALIAS_MARK_KEY = RESERVED_KEY_PREFIX + 'alias'
215
+ const OPTIONAL_MARK_KEY = RESERVED_KEY_PREFIX + 'optional'
216
+
217
+ // `{ %a } = @"f.aon"` is the pair `<head>: <include>`, so the head is
218
+ // one token and the grammar needs nothing new.
219
+ const IMPORT_HEAD_RE = new RegExp('^(' + ALIAS_SET + ')[ \\t]*=(?!=)')
220
+ const EXPORT_RE = /^export[ \t]*\([ \t]*([^()\s][^()]*?)[ \t]*\)/
199
221
 
200
222
  const CC_9 = 57
201
223
  const CC_A = 65
@@ -329,6 +351,72 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
329
351
  },
330
352
  })
331
353
 
354
+ // THE FIXED MATCHER'S CHECK RUNS AT EVERY POSITION: `{` opens a fixed
355
+ // token the text check never sees.
356
+ jsonic.options({
357
+ fixed: {
358
+ check: (lex: any) => {
359
+ const pnt = lex.pnt
360
+ const ex = lex.aontu_export
361
+
362
+ // The `(` is the pair's separator, the argument its value.
363
+ if (null != ex && pnt.sI === ex.cl) {
364
+ const cltkn = lex.token('#CL', undefined, '(', pnt)
365
+ pnt.cI += ex.arg - pnt.sI
366
+ pnt.sI = ex.arg
367
+ return { done: true, token: cltkn }
368
+ }
369
+
370
+ if (null != ex && pnt.sI === ex.arg) {
371
+ delete lex.aontu_export
372
+ const atkn = lex.token('#TX', ex.src, ex.src, pnt)
373
+ pnt.cI += ex.end - pnt.sI
374
+ pnt.sI = ex.end
375
+ return { done: true, token: atkn }
376
+ }
377
+
378
+ // THE SHORTHAND IS READ OFF THE SOURCE: a name lexes as its pair.
379
+ const sh = lex.aontu_shorthand
380
+ if (null != sh && pnt.sI === sh.at) {
381
+ if (1 === sh.stage) {
382
+ sh.stage = 2
383
+ return { done: true, token: lex.token('#CL', undefined, ':', pnt) }
384
+ }
385
+ delete lex.aontu_shorthand
386
+ return {
387
+ done: true, token: lex.token(
388
+ '#VL',
389
+ (r: Rule, ctx: JsonicContext) =>
390
+ addsite(new RefVal(
391
+ { peg: [aliasScopedKey(sh.name, srcUrl(ctx))], absolute: true }),
392
+ r, ctx),
393
+ sh.name, pnt)
394
+ }
395
+ }
396
+
397
+ if (CC_OB === lex.src.charCodeAt(pnt.sI)) {
398
+ const sres = ALIAS_SHORTHAND_RE.exec(lex.refwd())
399
+ if (null != sres) {
400
+ lex.aontu_shorthand_end = pnt.sI + sres[0].length
401
+ }
402
+ const hres = IMPORT_HEAD_RE.exec(lex.refwd())
403
+ if (null != hres) {
404
+ const hsrc = hres[1]
405
+ lex.aontu_eq_at = pnt.sI + hres[0].length - 1
406
+ const htkn = lex.token('#TX', hsrc, hsrc, pnt, {
407
+ aontu_import: aliasSetItems(hsrc) as AliasBind[],
408
+ })
409
+ pnt.sI += hsrc.length
410
+ pnt.cI += hsrc.length
411
+ return { done: true, token: htkn }
412
+ }
413
+ }
414
+
415
+ return undefined
416
+ },
417
+ },
418
+ })
419
+
332
420
  jsonic.options({
333
421
  text: {
334
422
  check: (lex: any) => {
@@ -349,10 +437,29 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
349
437
  lex.aontu_eq_at = j
350
438
  }
351
439
 
440
+ // The name is the KEY; separator and reference follow it.
441
+ const shend = lex.aontu_shorthand_end
442
+ if (null != shend && pnt.sI < shend) {
443
+ let k = pnt.sI + asrc.length
444
+ while (k < src.length && WS_RE.test(src[k])) {
445
+ k++
446
+ }
447
+ const ktkn = lex.token('#TX', asrc.substring(1), asrc, pnt)
448
+ pnt.cI += k - pnt.sI
449
+ pnt.sI = k
450
+ lex.aontu_shorthand = { at: k, name: asrc, stage: 1 }
451
+ if (shend - 1 <= k) {
452
+ delete lex.aontu_shorthand_end
453
+ }
454
+ return { done: true, token: ktkn }
455
+ }
456
+
352
457
  const atkn = lex.token(
353
458
  '#VL',
354
459
  (r: Rule, ctx: JsonicContext) =>
355
- addsite(new RefVal({ peg: [asrc], absolute: true }), r, ctx),
460
+ addsite(new RefVal(
461
+ { peg: [aliasScopedKey(asrc, srcUrl(ctx))], absolute: true }),
462
+ r, ctx),
356
463
  asrc,
357
464
  pnt)
358
465
  pnt.sI += asrc.length
@@ -360,6 +467,27 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
360
467
  return { done: true, token: atkn }
361
468
  }
362
469
 
470
+ // `export(...)`: the word is the key, spelled unwritably.
471
+ if (CC_e === src.charCodeAt(pnt.sI)) {
472
+ const eres = EXPORT_RE.exec(lex.refwd())
473
+ if (null != eres) {
474
+ const open = eres[0].indexOf('(')
475
+ const arg = eres[0].indexOf(eres[1], open)
476
+ lex.aontu_export = {
477
+ cl: pnt.sI + open,
478
+ arg: pnt.sI + arg,
479
+ end: pnt.sI + eres[0].length,
480
+ src: eres[1],
481
+ }
482
+ const etkn = lex.token(
483
+ '#TX', exportHoldKey(), EXPORT_DECL_NAME, pnt,
484
+ { aontu_export: true, aontu_export_items: aliasSetItems(eres[1]) })
485
+ pnt.sI += EXPORT_DECL_NAME.length
486
+ pnt.cI += EXPORT_DECL_NAME.length
487
+ return { done: true, token: etkn }
488
+ }
489
+ }
490
+
363
491
  if (CC_EQ === src.charCodeAt(pnt.sI) && lex.aontu_eq_at === pnt.sI) {
364
492
  delete lex.aontu_eq_at
365
493
  const eqtkn = lex.token('#CL', undefined, '=', pnt, { aontu_eq: true })
@@ -437,6 +565,28 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
437
565
  return v
438
566
  }
439
567
 
568
+ // `export` publishes a name; it renames none, and `{%}` is theirs.
569
+ const publishedNames = (items?: AliasBind[]): string[] | undefined =>
570
+ undefined === items || 0 === items.length ? undefined :
571
+ items.every((i) => i.local === i.remote) ?
572
+ items.map((i) => i.local) : undefined
573
+
574
+ // A ROOT MAY BE WRAPPED: a file's names are its MAP'S, not open()'s.
575
+ const declaringMap = (val: any): any => {
576
+ let cur = val
577
+ while (true === cur?.isFunc && 1 === cur.peg?.length) {
578
+ cur = cur.peg[0]
579
+ }
580
+ return cur
581
+ }
582
+
583
+ // The file a value was written in, and so any alias name's scope. A
584
+ // DOCUMENT IS ITS OWN SCOPE too, so a fileless parse is tagged.
585
+ const srcUrl = (ctx: JsonicContext): string => {
586
+ const url = ctx.meta.multisource && ctx.meta.multisource.path
587
+ return url ? url : ((ctx.meta as any).aontu_scope ??= '#' + (++SCOPE_SEQ))
588
+ }
589
+
440
590
  let addsite = (v: Val, r: Rule, ctx: JsonicContext) => {
441
591
  // The source text comes from the SAME token the row and column
442
592
  // come from. jsonic has carried it all along; not reading it is
@@ -452,8 +602,16 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
452
602
  const isAliasDecl = (ktkn: any, sep: any): boolean =>
453
603
  null != ktkn && VL === ktkn.tin && ALIAS_RE.test('' + ktkn.src) &&
454
604
  true === sep?.use?.aontu_eq
605
+ const keyName = (ktkn: any): string =>
606
+ 'string' === typeof ktkn?.val ? ktkn.val : '' + ktkn?.src
607
+
455
608
  const keyRefusalOf = (ktkn: any, sep: any):
456
609
  { why: string, details?: Record<string, any> } | undefined => {
610
+ // THE SENTINEL NAMESPACE IS THE ENGINE'S; it writes `export` here.
611
+ if (keyName(ktkn).startsWith(RESERVED_KEY_PREFIX) &&
612
+ true !== ktkn.use?.aontu_export) {
613
+ return { why: 'reserved_key' }
614
+ }
457
615
  if (null == ktkn || VL !== ktkn.tin) {
458
616
  return undefined
459
617
  }
@@ -579,14 +737,14 @@ help isolate the syntax error.`,
579
737
  ((prev as any)[SPREAD] || { o: '&', v: [] })
580
738
  ;(prev as any)[SPREAD].v.push(lm.spread.cj)
581
739
  }
582
- prev.___optional = (prev.___optional || [])
583
- for (const k of lm.optionalKeys) { prev.___optional.push(k) }
584
- prev.___alias = (prev.___alias || [])
585
- for (const k of lm.aliasKeys) { prev.___alias.push(k) }
740
+ prev[OPTIONAL_MARK_KEY] = (prev[OPTIONAL_MARK_KEY] || [])
741
+ for (const k of lm.optionalKeys) { prev[OPTIONAL_MARK_KEY].push(k) }
742
+ prev[ALIAS_MARK_KEY] = (prev[ALIAS_MARK_KEY] || [])
743
+ for (const k of lm.aliasKeys) { prev[ALIAS_MARK_KEY].push(k) }
586
744
  return prev
587
745
  }
588
- prev.___merge = (prev.___merge || [])
589
- prev.___merge.push(curr)
746
+ prev[MERGE_KEY] = (prev[MERGE_KEY] || [])
747
+ prev[MERGE_KEY].push(curr)
590
748
  return prev
591
749
  }
592
750
  }
@@ -1033,7 +1191,8 @@ help isolate the syntax error.`,
1033
1191
  const aname = r.u.aontu_alias_val
1034
1192
  if (null != aname && null != valnode) {
1035
1193
  const hoist = ((ctx as any).aontu_alias_hoist ||= [])
1036
- hoist.push({ name: '' + aname, val: valnode })
1194
+ hoist.push(
1195
+ { name: aliasScopedKey('' + aname, srcUrl(ctx)), val: valnode })
1037
1196
  }
1038
1197
 
1039
1198
  return undefined
@@ -1055,13 +1214,27 @@ help isolate the syntax error.`,
1055
1214
 
1056
1215
  .bc((r: Rule, ctx: JsonicContext) => {
1057
1216
  const optionalKeys = r.u.aontu_optional_keys ?? []
1058
- const aliasKeys = r.u.aontu_alias_keys ?? []
1217
+ const aliasDecls = r.u.aontu_alias_keys ?? []
1218
+ const aliasKeys: string[] = []
1219
+ const exportKeys: string[] = []
1059
1220
 
1060
1221
  let mo = r.node
1061
1222
 
1223
+ // A REFUSED KEY TAKES NO KEY and is not OPTIONAL: its name is a
1224
+ // mark's, and the elision below would put it back.
1225
+ for (const kr of (r.u.aontu_key_refusals ?? []) as any[]) {
1226
+ if ('reserved_key' === kr.why) {
1227
+ delete mo[kr.key]
1228
+ const oi = optionalKeys.indexOf(kr.key)
1229
+ if (-1 !== oi) {
1230
+ optionalKeys.splice(oi, 1)
1231
+ }
1232
+ }
1233
+ }
1234
+
1062
1235
  for (const k in mo) {
1063
- if (null == mo[k] && '___merge' !== k &&
1064
- '___optional' !== k && '___alias' !== k) {
1236
+ if (null == mo[k] && MERGE_KEY !== k &&
1237
+ OPTIONAL_MARK_KEY !== k && ALIAS_MARK_KEY !== k) {
1065
1238
  // Pathed at the KEY, not at the enclosing map. addsite takes
1066
1239
  // the rule's path, which here is the map's, so the error
1067
1240
  // would otherwise name the container and leave the reader to
@@ -1089,27 +1262,129 @@ help isolate the syntax error.`,
1089
1262
  return undefined
1090
1263
  }
1091
1264
 
1092
- for (const { key, tkn, why, details } of
1265
+ const reservedRefusals: Val[] = []
1266
+ for (const { key, tkn, why, details, url } of
1093
1267
  (r.u.aontu_key_refusals ?? []) as any[]) {
1094
1268
  const en: any = siteAt(addsite(new NilVal({ why }), r, ctx), tokenSite(tkn))
1095
1269
  if (null != details) {
1096
1270
  en.details = details
1097
1271
  }
1098
1272
  en.path = [...(r.k?.path ?? []), key]
1099
- mo[key] = en
1273
+ // A COLON DECLARATION IS REFUSED AND STILL NAMES SOMETHING, so
1274
+ // the refusal is held under the key that name would have had
1275
+ // and a use of it reports why; not an alias key, so it
1276
+ // generates unused.
1277
+ if ('alias_colon' === why) {
1278
+ delete mo[key]
1279
+ mo[aliasScopedKey(key, url)] = en
1280
+ }
1281
+ // A RESERVED KEY WAITS: its name is where the marks are kept.
1282
+ else if ('reserved_key' === why) {
1283
+ reservedRefusals.push(en)
1284
+ }
1285
+ else {
1286
+ mo[key] = en
1287
+ }
1288
+ }
1289
+
1290
+ // A NAME BELONGS TO THE FILE THAT DECLARED IT. Renamed here and
1291
+ // not at the key, so an elided value is still reported by the
1292
+ // name the source spells; rebuilt, as key order is resolution.
1293
+ const renamed = new Map<string, string>()
1294
+ for (const { name, key } of aliasDecls as any[]) {
1295
+ renamed.set(name, key)
1296
+ if (!aliasKeys.includes(key)) {
1297
+ aliasKeys.push(key)
1298
+ }
1299
+ }
1300
+ if (0 < renamed.size) {
1301
+ const entries = Object.entries(mo)
1302
+ for (const [k] of entries) {
1303
+ delete mo[k]
1304
+ }
1305
+ for (const [k, v] of entries) {
1306
+ const key = renamed.get(k)
1307
+ if (undefined === key) {
1308
+ mo[k] = v
1309
+ }
1310
+ else {
1311
+ mo[key] = v
1312
+ repathInstance(v, [...(r.k?.path ?? []), key])
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ // `export` PUBLISHES NAMES AND NOTHING ELSE; a bare name, a key
1318
+ // and the wildcard are refused, and the argument is erased.
1319
+ for (const { items, tkn } of (r.u.aontu_export_decls ?? []) as any[]) {
1320
+ delete mo[tkn.val]
1321
+ const names = publishedNames(items)
1322
+ if (undefined === names) {
1323
+ const en: any =
1324
+ siteAt(addsite(new NilVal({ why: 'export_arg' }), r, ctx), tokenSite(tkn))
1325
+ en.path = [...(r.k?.path ?? []), EXPORT_DECL_NAME]
1326
+ mo[EXPORT_DECL_NAME] = en
1327
+ }
1328
+ else {
1329
+ exportKeys.push(...names.map((n) => aliasScopedKey(n, srcUrl(ctx))))
1330
+ }
1331
+ }
1332
+
1333
+ // THE DESTRUCTURE IS ADDITIVE: the values land where the head
1334
+ // stands, each name binds in THIS file's scope, and a file
1335
+ // publishes only what IT declares. A DECLARATION IS THE
1336
+ // DOCUMENT'S, so the names go to the root and the values may
1337
+ // sit under a key.
1338
+ for (const im of (r.u.aontu_import_decls ?? []) as any[]) {
1339
+ const hoist = ((ctx as any).aontu_alias_hoist ||= [])
1340
+ const iv: any = mo[im.key]
1341
+ delete mo[im.key]
1342
+ const dv: any = declaringMap(iv)
1343
+ const ex: string[] = dv.exportKeys ?? []
1344
+ const ak: string[] = dv.aliasKeys ?? []
1345
+ for (const k of ak) {
1346
+ hoist.push({ name: k, val: dv.peg[k] })
1347
+ delete dv.peg[k]
1348
+ }
1349
+ if (0 < ak.length + ex.length) {
1350
+ dv.aliasKeys = []
1351
+ dv.exportKeys = []
1352
+ }
1353
+ repathInstance(iv, [...(r.k?.path ?? [])])
1354
+ ; (mo[MERGE_KEY] = mo[MERGE_KEY] || []).push(iv)
1355
+ const binds: AliasBind[] = 0 === im.names.length ?
1356
+ ex.map((k: string) =>
1357
+ ({ local: aliasBareName(k), remote: aliasBareName(k) })) : im.names
1358
+ for (const { local, remote } of binds) {
1359
+ const from = ex.find((k: string) => aliasBareName(k) === remote)
1360
+ let bind: Val
1361
+ if (undefined === from || !ak.includes(from)) {
1362
+ bind = siteAt(addsite(
1363
+ new NilVal({ why: 'import_not_exported' }), r, ctx), tokenSite(im.tkn))
1364
+ ; (bind as any).details = { name: remote }
1365
+ const en: any = siteAt(addsite(
1366
+ new NilVal({ why: 'import_not_exported' }), r, ctx), tokenSite(im.tkn))
1367
+ en.details = { name: remote }
1368
+ mo[MERGE_KEY].push(en)
1369
+ }
1370
+ else {
1371
+ bind = addsite(new RefVal({ peg: [from], absolute: true }), r, ctx)
1372
+ }
1373
+ hoist.push({ name: aliasScopedKey(local, im.url), val: bind })
1374
+ }
1100
1375
  }
1101
1376
 
1102
1377
  // Marks carried over from a map include folded in the merge
1103
1378
  // hook above, applied here where the MapVal is built.
1104
- if (mo.___optional || mo.___alias) {
1105
- for (const k of (mo.___optional || [])) {
1379
+ if (mo[OPTIONAL_MARK_KEY] || mo[ALIAS_MARK_KEY]) {
1380
+ for (const k of (mo[OPTIONAL_MARK_KEY] || [])) {
1106
1381
  if (!optionalKeys.includes(k)) { optionalKeys.push(k) }
1107
1382
  }
1108
- for (const k of (mo.___alias || [])) {
1383
+ for (const k of (mo[ALIAS_MARK_KEY] || [])) {
1109
1384
  if (!aliasKeys.includes(k)) { aliasKeys.push(k) }
1110
1385
  }
1111
- delete mo.___optional
1112
- delete mo.___alias
1386
+ delete mo[OPTIONAL_MARK_KEY]
1387
+ delete mo[ALIAS_MARK_KEY]
1113
1388
  }
1114
1389
 
1115
1390
  // A value-prefix declaration lands here, at the document root
@@ -1126,21 +1401,28 @@ help isolate the syntax error.`,
1126
1401
  }
1127
1402
 
1128
1403
  // Handle defered conjuncts, e.g. `{x:1 @"foo"}`
1129
- if (mo.___merge) {
1404
+ const deferred = mo[MERGE_KEY]
1405
+ delete mo[MERGE_KEY]
1406
+
1407
+ const merge = 0 < reservedRefusals.length ?
1408
+ [...(deferred ?? []), ...reservedRefusals] : deferred
1409
+
1410
+ if (merge) {
1130
1411
  let mop = { ...mo }
1131
- delete mop.___merge
1132
1412
 
1133
1413
  let mopv = new MapVal({ peg: mop })
1134
1414
  mopv.optionalKeys = optionalKeys
1135
1415
  mopv.aliasKeys = aliasKeys
1416
+ mopv.exportKeys = exportKeys
1136
1417
 
1137
1418
  r.node =
1138
- addsite(new ConjunctVal({ peg: [mopv, ...mo.___merge] }), r, ctx)
1419
+ addsite(new ConjunctVal({ peg: [mopv, ...merge] }), r, ctx)
1139
1420
  }
1140
1421
  else {
1141
1422
  r.node = addsite(new MapVal({ peg: mo }), r, ctx)
1142
1423
  r.node.optionalKeys = optionalKeys
1143
1424
  r.node.aliasKeys = aliasKeys
1425
+ r.node.exportKeys = exportKeys
1144
1426
  }
1145
1427
 
1146
1428
  return undefined
@@ -1169,7 +1451,7 @@ help isolate the syntax error.`,
1169
1451
  }
1170
1452
  }
1171
1453
 
1172
- // No ___merge arm here: the deferred map.merge that writes it
1454
+ // No merge-key arm here: the deferred map.merge that writes it
1173
1455
  // only ever fires for a `pair` rule, whose parent is always a
1174
1456
  // `map` rule with a plain-object node — never a list.
1175
1457
  {
@@ -1263,19 +1545,39 @@ help isolate the syntax error.`,
1263
1545
  }
1264
1546
  })
1265
1547
 
1266
- .bc((rule: Rule) => {
1548
+ .bc((rule: Rule, ctx: JsonicContext) => {
1267
1549
  // TRAVERSE PARENTS TO GET PATH
1268
1550
 
1269
- const ktkn: any = rule.o0
1551
+ // AN OPTIONAL PAIR OPENS ON ITS `?`: the rule before read the key.
1552
+ const ktkn: any = true === rule.prev?.u?.aontu_optional ?
1553
+ rule.prev.o0 : rule.o0
1270
1554
  const holder: any = rule.parent
1271
1555
  const kr = keyRefusalOf(ktkn, rule.o1)
1272
1556
  if (null != kr) {
1273
1557
  holder.u.aontu_key_refusals = (holder.u.aontu_key_refusals || [])
1274
- holder.u.aontu_key_refusals.push({ key: '' + ktkn.src, tkn: ktkn, ...kr })
1558
+ holder.u.aontu_key_refusals.push(
1559
+ { key: keyName(ktkn), tkn: ktkn, url: srcUrl(ctx), ...kr })
1275
1560
  }
1276
1561
  else if (isAliasDecl(ktkn, rule.o1)) {
1277
1562
  holder.u.aontu_alias_keys = (holder.u.aontu_alias_keys || [])
1278
- holder.u.aontu_alias_keys.push('' + ktkn.src)
1563
+ holder.u.aontu_alias_keys.push({
1564
+ name: '' + ktkn.src,
1565
+ key: aliasScopedKey('' + ktkn.src, srcUrl(ctx)),
1566
+ })
1567
+ }
1568
+ else if (true === ktkn?.use?.aontu_export) {
1569
+ holder.u.aontu_export_decls = (holder.u.aontu_export_decls || [])
1570
+ holder.u.aontu_export_decls.push(
1571
+ { items: ktkn.use.aontu_export_items, tkn: ktkn })
1572
+ }
1573
+ else if (null != ktkn?.use?.aontu_import) {
1574
+ holder.u.aontu_import_decls = (holder.u.aontu_import_decls || [])
1575
+ holder.u.aontu_import_decls.push({
1576
+ key: '' + ktkn.src,
1577
+ names: ktkn.use.aontu_import,
1578
+ url: srcUrl(ctx),
1579
+ tkn: ktkn,
1580
+ })
1279
1581
  }
1280
1582
 
1281
1583
  if (rule.u.spread) {
@@ -1394,7 +1696,8 @@ help isolate the syntax error.`,
1394
1696
  }
1395
1697
  // An element is a value position, so the prefix form applies.
1396
1698
  if (null == kr && isAliasDecl(ktkn, rule.o1)) {
1397
- ; ((ctx as any).aontu_alias_hoist ||= []).push({ name: key, val: v })
1699
+ ; ((ctx as any).aontu_alias_hoist ||= [])
1700
+ .push({ name: aliasScopedKey(key, srcUrl(ctx)), val: v })
1398
1701
  rule.node.push(v)
1399
1702
  return undefined
1400
1703
  }
@@ -1406,7 +1709,8 @@ help isolate the syntax error.`,
1406
1709
  // "is this the top level" test reads the path, so an element
1407
1710
  // of a top-level list must not read as the root.
1408
1711
  mv.path = [...(rule.k?.path ?? []), '' + rule.node.length]
1409
- if (true === rule.u.aontu_optional_elem) {
1712
+ // A REFUSED KEY IS NOT OPTIONAL: the refusal would be dropped.
1713
+ if (true === rule.u.aontu_optional_elem && null == kr) {
1410
1714
  mv.optionalKeys = [key]
1411
1715
  }
1412
1716
  rule.node.push(mv)