@hyperscale0/hsx 1.0.337 → 1.0.339

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 (51) hide show
  1. package/dist/src/ast.d.ts +1 -0
  2. package/dist/src/ast.d.ts.map +1 -1
  3. package/dist/src/ast.js.map +1 -1
  4. package/dist/src/attachment-economics.d.ts +37 -0
  5. package/dist/src/attachment-economics.d.ts.map +1 -1
  6. package/dist/src/attachment-economics.js +183 -3
  7. package/dist/src/attachment-economics.js.map +1 -1
  8. package/dist/src/binding-contract.d.ts.map +1 -1
  9. package/dist/src/binding-contract.js +10 -4
  10. package/dist/src/binding-contract.js.map +1 -1
  11. package/dist/src/catalog-program.d.ts +1 -1
  12. package/dist/src/catalog-program.d.ts.map +1 -1
  13. package/dist/src/catalog-program.js +1 -1
  14. package/dist/src/compile.d.ts.map +1 -1
  15. package/dist/src/compile.js +133 -41
  16. package/dist/src/compile.js.map +1 -1
  17. package/dist/src/examples-bundle.js +1 -1
  18. package/dist/src/examples-bundle.js.map +1 -1
  19. package/dist/src/headers.d.ts.map +1 -1
  20. package/dist/src/headers.js +3 -1
  21. package/dist/src/headers.js.map +1 -1
  22. package/dist/src/language.d.ts.map +1 -1
  23. package/dist/src/language.js +1 -0
  24. package/dist/src/language.js.map +1 -1
  25. package/dist/src/parse.js +13 -1
  26. package/dist/src/parse.js.map +1 -1
  27. package/dist/src/std-bundle.js +6 -6
  28. package/dist/src/std-bundle.js.map +1 -1
  29. package/dist/src/template-guide.d.ts.map +1 -1
  30. package/dist/src/template-guide.js +49 -18
  31. package/dist/src/template-guide.js.map +1 -1
  32. package/docs/README.md +5 -2
  33. package/examples/camera-rental.hsx +6 -1
  34. package/package.json +4 -4
  35. package/src/ast.ts +7 -1
  36. package/src/attachment-economics.ts +230 -2
  37. package/src/binding-contract.ts +13 -6
  38. package/src/catalog-program.ts +1 -1
  39. package/src/compile.ts +185 -58
  40. package/src/examples-bundle.ts +1 -1
  41. package/src/headers.ts +6 -1
  42. package/src/language.ts +1 -0
  43. package/src/parse.ts +16 -1
  44. package/src/std-bundle.ts +6 -6
  45. package/src/template-guide.ts +74 -22
  46. package/std/collections.hsx +4 -4
  47. package/std/financing.hsx +461 -266
  48. package/std/insurance.hsx +21 -5
  49. package/std/lending.hsx +13 -9
  50. package/std/money.hsx +148 -38
  51. package/std/reporting.hsx +6 -6
@@ -121,6 +121,7 @@ export function templateGuide(
121
121
  const compiled = compiledAttachment(program, example.attachment, options);
122
122
  const create = compiled.actions.create;
123
123
  const economics = compiled.warnings.flatMap((warning) =>
124
+ warning.code === "economic_purpose_missing" &&
124
125
  warning.message.startsWith(`${example.attachment}: `)
125
126
  ? [/economics [^}]+\}/.exec(warning.fix)![0]]
126
127
  : [],
@@ -163,23 +164,54 @@ function slot(block: BlockExpr, key: string) {
163
164
  return block.entries.find((entry) => entry.key === key)?.value;
164
165
  }
165
166
 
167
+ /** Each `key` row of a block with the `when` branches it sits under. */
168
+ function branched(
169
+ block: BlockExpr,
170
+ key: string,
171
+ outer: string[] = [],
172
+ ): { value: Expr; condition: string }[] {
173
+ return block.entries.flatMap((entry) =>
174
+ entry.key.startsWith("when ") && entry.value.kind === "block"
175
+ ? branched(entry.value, key, [...outer, entry.key])
176
+ : entry.key === key
177
+ ? [
178
+ {
179
+ value: entry.value,
180
+ condition: outer.length
181
+ ? `when ${outer.map((when) => when.slice(5)).join(" and ")}`
182
+ : "",
183
+ },
184
+ ]
185
+ : [],
186
+ );
187
+ }
188
+
189
+ /** A block's rows with every `when` branch's rows in place, so both forms read. */
190
+ function flat(block: BlockExpr): Entry[] {
191
+ return block.entries.flatMap((entry) =>
192
+ entry.key.startsWith("when ") && entry.value.kind === "block"
193
+ ? flat(entry.value)
194
+ : [entry],
195
+ );
196
+ }
197
+
166
198
  /**
167
199
  * The parser desugars `requires` and `moves` into blocks without source
168
- * spans, so each clause is spelled from the slot's own source text.
200
+ * spans, so each clause is spelled from the slot's own source text. A clause
201
+ * under a `when` branch names its condition.
169
202
  */
170
203
  function clauses(
171
- action: BlockExpr,
204
+ rows: { value: Expr; condition: string }[],
172
205
  key: "requires" | "moves",
173
206
  spell: (expression: Expr) => string,
174
207
  ) {
175
- return action.entries
176
- .filter((entry) => entry.key === key)
177
- .flatMap((entry) =>
178
- spell(entry.value)
179
- .split(new RegExp(`(?:^|[;,]?\\s+)${key}\\s+`))
180
- .map((clause) => clause.trim().replace(/[;,]$/, ""))
181
- .filter(Boolean),
182
- );
208
+ return rows.flatMap(({ value, condition }) =>
209
+ spell(value)
210
+ .split(new RegExp(`(?:^|[;,]?\\s+)${key}\\s+`))
211
+ .map((clause) => clause.trim().replace(/[;,]$/, ""))
212
+ .filter(Boolean)
213
+ .map((clause) => (condition ? `${clause}, ${condition}` : clause)),
214
+ );
183
215
  }
184
216
 
185
217
  function items(expression: Expr | undefined): Expr[] {
@@ -198,7 +230,7 @@ function guideActions(
198
230
  for (const entry of body.entries) {
199
231
  if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
200
232
  continue;
201
- const action = entry.value;
233
+ const action = { ...entry.value, entries: flat(entry.value) };
202
234
  const actor = slot(action, "actor");
203
235
  const party = actor?.kind === "block" ? slot(actor, "party") : undefined;
204
236
  const parent = actor?.kind === "block" ? slot(actor, "parent") : undefined;
@@ -219,10 +251,27 @@ function guideActions(
219
251
  };
220
252
  const due = at("due");
221
253
  const deadline = at("deadline");
222
- const requires = clauses(action, "requires", spell);
223
- const moves = clauses(action, "moves", spell);
224
- const invokes = items(slot(action, "invoke")).flatMap((item) =>
225
- item.kind === "block" ? [invocation(item, spell, prefix)] : [],
254
+ const requires = clauses(
255
+ branched(entry.value, "requires"),
256
+ "requires",
257
+ spell,
258
+ );
259
+ const moves = clauses(
260
+ action.entries
261
+ .filter((row) => row.key === "moves")
262
+ .map((row) => ({ value: row.value, condition: "" })),
263
+ "moves",
264
+ spell,
265
+ );
266
+ const invokes = branched(entry.value, "invoke").flatMap(
267
+ ({ value, condition }) =>
268
+ items(value).flatMap((item) =>
269
+ item.kind === "block"
270
+ ? [
271
+ `${invocation(item, spell, prefix)}${condition ? `, ${condition}` : ""}`,
272
+ ]
273
+ : [],
274
+ ),
226
275
  );
227
276
  const from = slot(action, "from");
228
277
  const to = slot(action, "to");
@@ -262,7 +311,9 @@ function invocation(
262
311
  ) {
263
312
  const instrument = slot(block, "instrument");
264
313
  const action = slot(block, "action");
265
- const target = `${instrument ? `${prefix}${spell(instrument)}.` : ""}${action ? spell(action) : "?"}`;
314
+ // current() is the record itself, already named by the prefix.
315
+ const own = instrument && spell(instrument) === "current()";
316
+ const target = `${instrument && !own ? `${prefix}${spell(instrument)}.` : own ? prefix : ""}${action ? spell(action) : "?"}`;
266
317
  const range = slot(block, "range");
267
318
  const count = range?.kind === "block" ? slot(range, "count") : undefined;
268
319
  const literal =
@@ -351,7 +402,7 @@ function valueMeaning(
351
402
  const fields = slot(decl.body, "fields");
352
403
  const set =
353
404
  fields?.kind === "block"
354
- ? fields.entries.filter((entry) => bare.test(fieldValue(entry, spell)))
405
+ ? flat(fields).filter((entry) => bare.test(fieldValue(entry, spell)))
355
406
  : [];
356
407
  const read = new Set(set.map((entry) => entry.key));
357
408
  const uses: string[] = [];
@@ -367,14 +418,14 @@ function valueMeaning(
367
418
  : new RegExp(`\\bself\\.${field}\\b`).test(text),
368
419
  );
369
420
  if (own?.kind === "block" && prefix)
370
- for (const entry of own.entries) {
421
+ for (const entry of flat(own)) {
371
422
  const value = fieldValue(entry, spell);
372
423
  if (reads(value)) uses.push(`${prefix}${entry.key}: ${value}`);
373
424
  }
374
425
  for (const entry of body.entries) {
375
426
  if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
376
427
  continue;
377
- for (const part of entry.value.entries) {
428
+ for (const part of flat(entry.value)) {
378
429
  if (["actor", "from", "to", "summary"].includes(part.key)) continue;
379
430
  const text = spell(part.value);
380
431
  // A long slot, such as an invoke list, is named rather than spelled.
@@ -391,7 +442,7 @@ function valueMeaning(
391
442
  const recordFields = slot(record.value, "fields");
392
443
  const refs =
393
444
  recordFields?.kind === "block"
394
- ? recordFields.entries
445
+ ? flat(recordFields)
395
446
  .filter((entry) =>
396
447
  /^ref<parent>/.test(fieldValue(entry, spell)),
397
448
  )
@@ -409,7 +460,8 @@ function valueMeaning(
409
460
  : []),
410
461
  ...(set.length
411
462
  ? [
412
- `Sets ${set.map((entry) => `${entry.key}: ${fieldValue(entry, spell)}`).join(", ")}.`,
463
+ // Two branches can set one field the same way.
464
+ `Sets ${[...new Set(set.map((entry) => `${entry.key}: ${fieldValue(entry, spell)}`))].join(", ")}.`,
413
465
  ]
414
466
  : []),
415
467
  ...(uses.length ? [`Read by ${uses.slice(0, 6).join("; ")}.`] : []),
@@ -589,7 +641,7 @@ function listBound(
589
641
  const fields = slot(decl.body, "fields");
590
642
  const entry =
591
643
  fields?.kind === "block"
592
- ? fields.entries.find((item) => item.key === name)
644
+ ? flat(fields).find((item) => item.key === name)
593
645
  : undefined;
594
646
  const bound = entry
595
647
  ? /^list\(\s*[\w<>]+\s*,\s*(\w+)\s*\)$/.exec(fieldValue(entry, spell))?.[1]
@@ -26,7 +26,7 @@ instrument case(on: ref<financing.installments>[], agency: party, capital: party
26
26
  action create {
27
27
  due: { at: self.eligibleAt }
28
28
  requires self.plan == self.slice.plan
29
- requires self.slice.principalReceivable.balance > "0"
29
+ requires self.slice.openPrincipal > "0"
30
30
  requires unique "overdue_referral" on [self.slice]
31
31
  }
32
32
  action assign { from: opened, to: assigned, actor: { party: capital } }
@@ -74,17 +74,17 @@ instrument reminder(on: ref<financing.installments>[], operator: party = program
74
74
  action before_due {
75
75
  from: ready, to: ready, actor: clock
76
76
  due: { at: self.slice.dueAt, localDay: { days: before_days, direction: "before", hour: send_from, timezone: timezone } }
77
- invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.principalReceivable.balance }, operator: ">", right: { literal: "0" } }, input: {} }]
77
+ invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.openPrincipal }, operator: ">", right: { literal: "0" } }, input: {} }]
78
78
  }
79
79
  action overdue {
80
80
  from: ready, to: ready, actor: clock
81
81
  due: { at: self.slice.dueAt, localDay: { days: overdue_days, direction: "after", hour: send_from, timezone: timezone } }
82
- invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.principalReceivable.balance }, operator: ">", right: { literal: "0" } }, input: {} }]
82
+ invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.openPrincipal }, operator: ">", right: { literal: "0" } }, input: {} }]
83
83
  }
84
84
  action send {
85
85
  from: ready, to: ready, actor: { parent: current() }
86
86
  requires self.slice.plan in [active]
87
- requires self.slice.principalReceivable.balance > "0"
87
+ requires self.slice.openPrincipal > "0"
88
88
  reminder: { installment: self.slice, recipient: self.slice.plan.borrower, dueAt: self.slice.dueAt, channel: "email", template: "payment_reminder", beforeDays: before_days, overdueDays: overdue_days, maxPerDay: max_per_day, startHour: send_from, endHour: send_until, timezone: timezone }
89
89
  }
90
90
  }