@lokascript/framework 4.7.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/api/create-dsl.d.ts +5 -2
  3. package/dist/api/create-dsl.d.ts.map +1 -1
  4. package/dist/api/index.js +45 -5
  5. package/dist/api/index.js.map +1 -1
  6. package/dist/core/index.js +10 -0
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/matching/stock-reader.d.ts +8 -1
  9. package/dist/core/matching/stock-reader.d.ts.map +1 -1
  10. package/dist/core/pattern-matching/index.js.map +1 -1
  11. package/dist/core/types.d.ts +5 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/core/types.js.map +1 -1
  14. package/dist/generation/index.js +12 -2
  15. package/dist/generation/index.js.map +1 -1
  16. package/dist/index.cjs +45 -5
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +45 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/ir/index.js.map +1 -1
  21. package/dist/parsing/index.js.map +1 -1
  22. package/dist/testing/gate/index.js +44 -12
  23. package/dist/testing/gate/index.js.map +1 -1
  24. package/dist/testing/gate/round-trip.d.ts +24 -3
  25. package/dist/testing/gate/round-trip.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/api/create-dsl-engine.test.ts +65 -1
  28. package/src/api/create-dsl.test.ts +7 -0
  29. package/src/api/create-dsl.ts +32 -4
  30. package/src/core/matching/stock-reader.test.ts +87 -3
  31. package/src/core/matching/stock-reader.ts +20 -1
  32. package/src/core/types.ts +5 -0
  33. package/src/generation/schema-patterns.test.ts +41 -1
  34. package/src/generation/schema-patterns.ts +12 -0
  35. package/src/testing/gate/round-trip.test.ts +99 -9
  36. package/src/testing/gate/round-trip.ts +76 -11
package/dist/index.js CHANGED
@@ -1706,6 +1706,11 @@ function readGreedy(tokens, stopMarkers) {
1706
1706
  }
1707
1707
  return values.length > 0 ? { type: "expression", raw: values.join(" ") } : null;
1708
1708
  }
1709
+ function isListedWord(token, words) {
1710
+ if (!token) return false;
1711
+ const forms = [token.value, token.normalized].filter((f) => typeof f === "string");
1712
+ return forms.some((form) => words.some((word) => word.toLowerCase() === form.toLowerCase()));
1713
+ }
1709
1714
  var StockValueReader = class {
1710
1715
  constructor(view, profiles = {}) {
1711
1716
  this.view = view;
@@ -1738,6 +1743,11 @@ var StockValueReader = class {
1738
1743
  const token = tokens.peek();
1739
1744
  if (token && this.view.wouldMatch(next, token)) return false;
1740
1745
  }
1746
+ if (slot.words && !isListedWord(tokens.peek(), slot.words)) return slot.optional ?? false;
1747
+ if (slot.optional && !slot.greedy) {
1748
+ const token = tokens.peek();
1749
+ if (token && isStopMarker(token, greedyStopMarkers(this.view.patternRest()))) return true;
1750
+ }
1741
1751
  if (slot.greedy) {
1742
1752
  const value = readGreedy(tokens, greedyStopMarkers(this.view.patternRest()));
1743
1753
  if (value) {
@@ -2143,11 +2153,13 @@ function generateVerbFirstPattern(schema, profile, config = defaultConfig2) {
2143
2153
  const verbToken = keyword.alternatives ? { type: "literal", value: keyword.primary, alternatives: keyword.alternatives } : { type: "literal", value: keyword.primary };
2144
2154
  const roleTokens = requiredRoles.flatMap((r) => {
2145
2155
  const prefix = r.valuePrefixLiteral?.[profile.code];
2156
+ const words = wordsFor(r, profile.code);
2146
2157
  const roleToken = {
2147
2158
  type: "role",
2148
2159
  role: r.role,
2149
2160
  optional: false,
2150
- expectedTypes: r.expectedTypes
2161
+ expectedTypes: r.expectedTypes,
2162
+ ...words ? { words } : {}
2151
2163
  };
2152
2164
  return prefix ? [{ type: "literal", value: prefix }, roleToken] : [roleToken];
2153
2165
  });
@@ -2317,11 +2329,17 @@ function buildRoleTokens(grammar, schema, profile) {
2317
2329
  }
2318
2330
  return tokens;
2319
2331
  }
2332
+ function wordsFor(roleSpec, language) {
2333
+ if (!roleSpec.words) return void 0;
2334
+ const words = [...roleSpec.words["*"] ?? [], ...roleSpec.words[language] ?? []];
2335
+ return words.length > 0 ? words : void 0;
2336
+ }
2320
2337
  function buildRoleToken(roleSpec, profile) {
2321
2338
  const tokens = [];
2322
2339
  const overrideMarker = roleSpec.markerOverride?.[profile.code];
2323
2340
  const defaultMarker = profile.roleMarkers[roleSpec.role];
2324
2341
  const suppressMarker = roleSpec.renderOverride?.[profile.code] === "";
2342
+ const words = wordsFor(roleSpec, profile.code);
2325
2343
  const roleValueToken = {
2326
2344
  type: "role",
2327
2345
  role: roleSpec.role,
@@ -2333,7 +2351,9 @@ function buildRoleToken(roleSpec, profile) {
2333
2351
  ...roleSpec.valueShape !== void 0 ? { valueShape: roleSpec.valueShape } : {},
2334
2352
  // A greedy role reads up to the next marker the pattern owes (a domain's
2335
2353
  // SQL condition); its value reader does the reading.
2336
- ...roleSpec.greedy ? { greedy: true } : {}
2354
+ ...roleSpec.greedy ? { greedy: true } : {},
2355
+ // A role with a word list reads only one of its words.
2356
+ ...words ? { words } : {}
2337
2357
  };
2338
2358
  const prefixLiteral = roleSpec.valuePrefixLiteral?.[profile.code];
2339
2359
  const pushPrefixed = () => {
@@ -3889,12 +3909,15 @@ function readingDifference(source, back) {
3889
3909
 
3890
3910
  // src/api/create-dsl.ts
3891
3911
  var UnreadInputError = class extends Error {
3892
- constructor(input, language, reading, unread) {
3893
- super(`"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread`);
3912
+ constructor(input, language, reading, unread, hint) {
3913
+ super(
3914
+ `"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread` + (hint ? `; ${hint}` : "")
3915
+ );
3894
3916
  this.input = input;
3895
3917
  this.language = language;
3896
3918
  this.reading = reading;
3897
3919
  this.unread = unread;
3920
+ this.hint = hint;
3898
3921
  this.code = "unread-input";
3899
3922
  this.name = "UnreadInputError";
3900
3923
  }
@@ -4013,15 +4036,32 @@ var MultilingualDSLImpl = class {
4013
4036
  }
4014
4037
  if (longestPartial) {
4015
4038
  const unread = tokens.tokens.slice(longestPartial.consumedTokens).map((t) => t.value).join(" ");
4039
+ const hint = this.quoteHint(longestPartial, tokens.tokens);
4016
4040
  throw new UnreadInputError(
4017
4041
  input,
4018
4042
  language,
4019
4043
  this.nodeOf(longestPartial, input, language),
4020
- unread
4044
+ unread,
4045
+ ...hint ? [hint] : []
4021
4046
  );
4022
4047
  }
4023
4048
  throw new Error(`No pattern matched for input: ${input}`);
4024
4049
  }
4050
+ /**
4051
+ * `if "buy milk" is one value, quote it`, where the last token a match read is a bare word it took
4052
+ * as a role's value (a role with no word list) and the first it left is a bare word too.
4053
+ */
4054
+ quoteHint(match, tokens) {
4055
+ const last = tokens[match.consumedTokens - 1];
4056
+ const next = tokens[match.consumedTokens];
4057
+ if (last?.kind !== "identifier" || next?.kind !== "identifier") return void 0;
4058
+ const schema = this.schemaLookup.getSchema(match.pattern.command);
4059
+ const role = [...match.captured].find(
4060
+ ([, v]) => v.raw === last.value
4061
+ )?.[0];
4062
+ if (!role || schema?.roles.find((r) => r.role === role)?.words) return void 0;
4063
+ return `if "${last.value} ${next.value}" is one value, quote it`;
4064
+ }
4025
4065
  nodeOf(match, input, language) {
4026
4066
  return {
4027
4067
  kind: "command",