@rcrsr/rill 0.16.0 → 0.17.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 (57) hide show
  1. package/README.md +37 -21
  2. package/dist/ext/crypto/index.d.ts +3 -3
  3. package/dist/ext/crypto/index.js +61 -58
  4. package/dist/ext/exec/index.d.ts +3 -3
  5. package/dist/ext/exec/index.js +14 -8
  6. package/dist/ext/fetch/index.d.ts +3 -3
  7. package/dist/ext/fetch/index.js +16 -11
  8. package/dist/ext/fs/index.d.ts +3 -3
  9. package/dist/ext/fs/index.js +242 -239
  10. package/dist/ext/kv/index.d.ts +3 -3
  11. package/dist/ext/kv/index.js +197 -195
  12. package/dist/ext/kv/store.js +2 -1
  13. package/dist/ext-parse-bridge.d.ts +10 -0
  14. package/dist/ext-parse-bridge.js +10 -0
  15. package/dist/generated/introspection-data.d.ts +1 -1
  16. package/dist/generated/introspection-data.js +385 -296
  17. package/dist/generated/version-data.d.ts +1 -1
  18. package/dist/generated/version-data.js +2 -2
  19. package/dist/index.d.ts +15 -4
  20. package/dist/index.js +14 -5
  21. package/dist/parser/parser-types.js +12 -0
  22. package/dist/parser/parser-use.js +7 -1
  23. package/dist/runtime/core/callable.d.ts +20 -8
  24. package/dist/runtime/core/callable.js +63 -23
  25. package/dist/runtime/core/context.d.ts +0 -11
  26. package/dist/runtime/core/context.js +76 -75
  27. package/dist/runtime/core/eval/index.d.ts +2 -2
  28. package/dist/runtime/core/eval/index.js +11 -0
  29. package/dist/runtime/core/eval/mixins/closures.js +15 -15
  30. package/dist/runtime/core/eval/mixins/conversion.js +51 -110
  31. package/dist/runtime/core/eval/mixins/core.js +2 -2
  32. package/dist/runtime/core/eval/mixins/expressions.js +35 -27
  33. package/dist/runtime/core/eval/mixins/literals.js +3 -3
  34. package/dist/runtime/core/eval/mixins/types.js +44 -54
  35. package/dist/runtime/core/eval/mixins/variables.js +10 -8
  36. package/dist/runtime/core/field-descriptor.d.ts +3 -3
  37. package/dist/runtime/core/field-descriptor.js +2 -1
  38. package/dist/runtime/core/introspection.js +6 -6
  39. package/dist/runtime/core/markers.d.ts +12 -0
  40. package/dist/runtime/core/markers.js +7 -0
  41. package/dist/runtime/core/type-registrations.d.ts +136 -0
  42. package/dist/runtime/core/type-registrations.js +749 -0
  43. package/dist/runtime/core/type-structures.d.ts +128 -0
  44. package/dist/runtime/core/type-structures.js +12 -0
  45. package/dist/runtime/core/types.d.ts +15 -3
  46. package/dist/runtime/core/values.d.ts +62 -153
  47. package/dist/runtime/core/values.js +308 -524
  48. package/dist/runtime/ext/builtins.js +83 -64
  49. package/dist/runtime/ext/extensions.d.ts +30 -124
  50. package/dist/runtime/ext/extensions.js +0 -93
  51. package/dist/runtime/ext/test-context.d.ts +28 -0
  52. package/dist/runtime/ext/test-context.js +154 -0
  53. package/dist/runtime/index.d.ts +22 -8
  54. package/dist/runtime/index.js +18 -4
  55. package/dist/signature-parser.d.ts +2 -2
  56. package/dist/signature-parser.js +14 -14
  57. package/package.json +1 -1
@@ -70,6 +70,17 @@ export function assertType(value, expected, location) {
70
70
  resolverConfigs: new Map(),
71
71
  resolvingSchemes: new Set(),
72
72
  typeMethodDicts: new Map(),
73
+ leafTypes: new Set([
74
+ 'string',
75
+ 'number',
76
+ 'bool',
77
+ 'vector',
78
+ 'type',
79
+ 'any',
80
+ 'closure',
81
+ 'field_descriptor',
82
+ ]),
83
+ unvalidatedMethodReceivers: new Set(),
73
84
  };
74
85
  const evaluator = getEvaluator(minimalContext);
75
86
  return evaluator.assertType(value, expected, location);
@@ -43,8 +43,8 @@
43
43
  */
44
44
  import { RillError, RuntimeError } from '../../../../types.js';
45
45
  import { isCallable, isScriptCallable, isApplicationCallable, isDict, marshalArgs, } from '../../callable.js';
46
- import { getVariable, pushCallFrame, popCallFrame, UNVALIDATED_METHOD_PARAMS, UNVALIDATED_METHOD_RECEIVERS, } from '../../context.js';
47
- import { inferType, isTypeValue, isTuple, isOrdered, paramToFieldDef, inferStructuralType, structuralTypeMatches, formatStructuralType, anyTypeValue, rillTypeToTypeValue, } from '../../values.js';
46
+ import { getVariable, pushCallFrame, popCallFrame, UNVALIDATED_METHOD_PARAMS, } from '../../context.js';
47
+ import { inferType, isTypeValue, isTuple, isOrdered, paramToFieldDef, inferStructure, structureMatches, formatStructure, anyTypeValue, structureToTypeValue, } from '../../values.js';
48
48
  /**
49
49
  * ClosuresMixin implementation.
50
50
  *
@@ -215,8 +215,8 @@ function createClosuresMixin(Base) {
215
215
  validateParamType(param, value, callLocation) {
216
216
  if (param.type === undefined)
217
217
  return;
218
- if (!structuralTypeMatches(value, param.type)) {
219
- const expectedType = formatStructuralType(param.type);
218
+ if (!structureMatches(value, param.type)) {
219
+ const expectedType = formatStructure(param.type);
220
220
  const actualType = inferType(value);
221
221
  throw new RuntimeError('RILL-R001', `Parameter type mismatch: ${param.name} expects ${expectedType}, got ${actualType}`, callLocation, { paramName: param.name, expectedType, actualType });
222
222
  }
@@ -541,13 +541,13 @@ function createClosuresMixin(Base) {
541
541
  throw new RuntimeError('RILL-R003', `Method .${node.name} not available on callable (invoke with -> $() first)`, this.getNodeLocation(node), { methodName: node.name, receiverType: 'callable' });
542
542
  }
543
543
  // IR-3: .name on type values returns the typeName string (method path)
544
- // IR-4: .signature on type values returns formatStructuralType(structure)
544
+ // IR-4: .signature on type values returns formatStructure(structure)
545
545
  if (isTypeValue(receiver)) {
546
546
  if (node.name === 'name') {
547
547
  return receiver.typeName;
548
548
  }
549
549
  if (node.name === 'signature') {
550
- return formatStructuralType(receiver.structure);
550
+ return formatStructure(receiver.structure);
551
551
  }
552
552
  }
553
553
  const args = await this.evaluateArgs(node.args);
@@ -617,10 +617,10 @@ function createClosuresMixin(Base) {
617
617
  throw new RuntimeError('RILL-R009', `Property '${node.name}' not found on type value (available: name, signature)`, this.getNodeLocation(node), { property: node.name, type: 'type value' });
618
618
  }
619
619
  // RILL-R003: method exists on other types but not this receiver's type.
620
- // Methods in UNVALIDATED_METHOD_RECEIVERS handle their own receiver validation
620
+ // Methods in unvalidatedMethodReceivers handle their own receiver validation
621
621
  // with specific error messages; skip generic RILL-R003 for them and let the
622
622
  // method body run its own check (they exist in at least one type dict).
623
- if (!UNVALIDATED_METHOD_RECEIVERS.has(node.name)) {
623
+ if (!this.ctx.unvalidatedMethodReceivers.has(node.name)) {
624
624
  const supportedTypes = [];
625
625
  for (const [dictType, dict] of this.ctx.typeMethodDicts) {
626
626
  if (dict[node.name] !== undefined) {
@@ -632,14 +632,14 @@ function createClosuresMixin(Base) {
632
632
  }
633
633
  }
634
634
  else {
635
- // UNVALIDATED_METHOD_RECEIVERS: dispatch to the method in ANY type dict so the
635
+ // unvalidatedMethodReceivers: dispatch to the method in ANY type dict so the
636
636
  // method body can run its own custom receiver validation and error message.
637
637
  for (const [, dict] of this.ctx.typeMethodDicts) {
638
638
  const fallbackMethod = dict[node.name];
639
639
  if (fallbackMethod !== undefined &&
640
640
  isApplicationCallable(fallbackMethod)) {
641
641
  try {
642
- // UNVALIDATED_METHOD_RECEIVERS handle their own receiver validation.
642
+ // Unvalidated methods handle their own receiver validation.
643
643
  // Build named record with receiver so buildMethodEntry extracts it correctly;
644
644
  // the method body performs its own receiver type check with a custom error.
645
645
  const fbMethodArgs = { receiver };
@@ -706,7 +706,7 @@ function createClosuresMixin(Base) {
706
706
  const typeValue = Object.freeze({
707
707
  __rill_type: true,
708
708
  typeName: inferType(value),
709
- structure: inferStructuralType(value),
709
+ structure: inferStructure(value),
710
710
  });
711
711
  return typeValue;
712
712
  }
@@ -728,10 +728,10 @@ function createClosuresMixin(Base) {
728
728
  if (key === 'input') {
729
729
  // Untyped host callables have params set to undefined at runtime (see callable() factory)
730
730
  if (value.params === undefined) {
731
- return rillTypeToTypeValue({ type: 'ordered', fields: [] });
731
+ return structureToTypeValue({ kind: 'ordered', fields: [] });
732
732
  }
733
- const fields = value.params.map((param) => paramToFieldDef(param.name, param.type ?? { type: 'any' }, param.defaultValue));
734
- return rillTypeToTypeValue({ type: 'ordered', fields });
733
+ const fields = value.params.map((param) => paramToFieldDef(param.name, param.type ?? { kind: 'any' }, param.defaultValue));
734
+ return structureToTypeValue({ kind: 'ordered', fields });
735
735
  }
736
736
  // IR-3: ^output reads callable.returnType directly for all kinds
737
737
  if (key === 'output') {
@@ -894,7 +894,7 @@ function createClosuresMixin(Base) {
894
894
  const paramEntry = {};
895
895
  // Add type field if param has type annotation
896
896
  if (param.type !== undefined) {
897
- paramEntry['type'] = formatStructuralType(param.type);
897
+ paramEntry['type'] = formatStructure(param.type);
898
898
  }
899
899
  // Add __annotations field if param has parameter-level annotations
900
900
  if (Object.keys(param.annotations).length > 0) {
@@ -27,8 +27,9 @@
27
27
  * @internal
28
28
  */
29
29
  import { RuntimeError } from '../../../../types.js';
30
- import { inferType, isTuple, isOrdered, isTypeValue, createOrdered, createTuple, formatValue, deepCopyRillValue, hasCollectionFields, emptyForType, } from '../../values.js';
30
+ import { inferType, isTuple, isOrdered, isTypeValue, createOrdered, createTuple, deepCopyRillValue, hasCollectionFields, emptyForType, } from '../../values.js';
31
31
  import { isDict } from '../../callable.js';
32
+ import { BUILT_IN_TYPES } from '../../type-registrations.js';
32
33
  import { getVariable } from '../../context.js';
33
34
  /**
34
35
  * ConversionMixin implementation.
@@ -106,7 +107,15 @@ function createConversionMixin(Base) {
106
107
  }
107
108
  /**
108
109
  * Apply conversion from source value to target type name.
109
- * Implements the compatibility matrix from IR-9.
110
+ * Dispatches to protocol.convertTo on the source type's registration.
111
+ *
112
+ * IR-6: Replaces the hardcoded conversion matrix with protocol dispatch.
113
+ *
114
+ * Special cases preserved:
115
+ * - Same type = no-op (short-circuit)
116
+ * - dict -> :>ordered without structural sig raises RILL-R037 (EC-11)
117
+ * - String-to-number parse failure raises RILL-R038 (EC-12)
118
+ * - Missing convertTo target raises RILL-R036 (EC-10)
110
119
  */
111
120
  applyConversion(input, targetType, node) {
112
121
  const sourceType = inferType(input);
@@ -114,105 +123,34 @@ function createConversionMixin(Base) {
114
123
  if (sourceType === targetType) {
115
124
  return input;
116
125
  }
117
- // Apply compatibility matrix
118
- switch (targetType) {
119
- case 'list':
120
- return this.convertToList(input, sourceType, node);
121
- case 'dict':
122
- return this.convertToDict(input, sourceType, node);
123
- case 'tuple':
124
- return this.convertToTuple(input, sourceType, node);
125
- case 'ordered':
126
- // dict -> :>ordered without structural sig is always a runtime error (EC-11)
127
- if (sourceType === 'dict') {
128
- throw new RuntimeError('RILL-R037', 'dict to ordered conversion requires structural type signature', this.getNodeLocation(node));
129
- }
130
- return this.convertToOrdered(input, sourceType, node);
131
- case 'number':
132
- return this.convertToNumber(input, sourceType, node);
133
- case 'string':
134
- return this.convertToString(input, sourceType, node);
135
- case 'bool':
136
- return this.convertToBoolean(input, sourceType, node);
137
- default:
138
- this.throwIncompatible(sourceType, targetType, node);
126
+ // dict -> :>ordered without structural sig is always RILL-R037 (EC-11)
127
+ if (sourceType === 'dict' && targetType === 'ordered') {
128
+ throw new RuntimeError('RILL-R037', 'dict to ordered conversion requires structural type signature', this.getNodeLocation(node));
139
129
  }
140
- // TypeScript exhaustiveness
141
- return input;
142
- }
143
- /** Convert to list type. Valid source: tuple. */
144
- convertToList(input, sourceType, node) {
145
- if (isTuple(input)) {
146
- return input.entries;
130
+ // Find source type registration and dispatch via protocol.convertTo
131
+ const reg = BUILT_IN_TYPES.find((r) => r.name === sourceType);
132
+ const converter = reg?.protocol.convertTo?.[targetType];
133
+ if (!converter) {
134
+ throw new RuntimeError('RILL-R036', `cannot convert ${sourceType} to ${targetType}`, this.getNodeLocation(node), { source: sourceType, target: targetType });
147
135
  }
148
- this.throwIncompatible(sourceType, 'list', node);
149
- return input;
150
- }
151
- /** Convert to dict type. Valid source: ordered. */
152
- convertToDict(input, sourceType, node) {
153
- if (isOrdered(input)) {
154
- const result = {};
155
- for (const [key, value] of input.entries) {
156
- result[key] = value;
157
- }
158
- return result;
136
+ try {
137
+ return converter(input);
159
138
  }
160
- this.throwIncompatible(sourceType, 'dict', node);
161
- return input;
162
- }
163
- /** Convert to tuple type. Valid source: list. */
164
- convertToTuple(input, sourceType, node) {
165
- if (Array.isArray(input) && !isTuple(input) && !isOrdered(input)) {
166
- return createTuple(input);
167
- }
168
- this.throwIncompatible(sourceType, 'tuple', node);
169
- return input;
170
- }
171
- /** Convert to ordered type. Valid source: dict (with sig, handled separately). */
172
- convertToOrdered(input, sourceType, node) {
173
- // Only dict -> ordered is valid, but it requires a sig (checked by caller)
174
- this.throwIncompatible(sourceType, 'ordered', node);
175
- return input;
176
- }
177
- /** Convert to number type. Valid source: string (parseable) or bool. */
178
- convertToNumber(input, sourceType, node) {
179
- if (sourceType === 'string') {
180
- const str = input;
181
- const parsed = Number(str);
182
- if (isNaN(parsed) || str.trim() === '') {
183
- throw new RuntimeError('RILL-R038', `cannot convert string "${str}" to number`, this.getNodeLocation(node), { value: str });
139
+ catch (err) {
140
+ // Protocol converters throw plain Errors; wrap as RuntimeError
141
+ // with the appropriate error code.
142
+ if (err instanceof RuntimeError)
143
+ throw err;
144
+ // String-to-number parse failures use RILL-R038 (EC-12)
145
+ // Preserve the protocol's detailed message (includes unparseable value).
146
+ if (sourceType === 'string' && targetType === 'number') {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ throw new RuntimeError('RILL-R038', message, this.getNodeLocation(node), { value: input });
184
149
  }
185
- return parsed;
150
+ // All other conversion failures use RILL-R036 (EC-10)
151
+ // Use consistent "cannot convert X to Y" format.
152
+ throw new RuntimeError('RILL-R036', `cannot convert ${sourceType} to ${targetType}`, this.getNodeLocation(node), { source: sourceType, target: targetType });
186
153
  }
187
- if (sourceType === 'bool') {
188
- return input ? 1 : 0;
189
- }
190
- this.throwIncompatible(sourceType, 'number', node);
191
- return input;
192
- }
193
- /** Convert to bool type. Valid source: number (0 or 1) or string ("true" or "false"). */
194
- convertToBoolean(input, sourceType, node) {
195
- if (sourceType === 'number') {
196
- const n = input;
197
- if (n === 0)
198
- return false;
199
- if (n === 1)
200
- return true;
201
- this.throwIncompatible(sourceType, 'bool', node);
202
- }
203
- if (sourceType === 'string') {
204
- const s = input;
205
- if (s === 'true')
206
- return true;
207
- if (s === 'false')
208
- return false;
209
- this.throwIncompatible(sourceType, 'bool', node);
210
- }
211
- this.throwIncompatible(sourceType, 'bool', node);
212
- }
213
- /** Convert to string type. Valid source: any type via formatValue semantics. */
214
- convertToString(input, _sourceType, _node) {
215
- return formatValue(input);
216
154
  }
217
155
  /**
218
156
  * Convert dict -> :>ordered(field: type = default, ...) using structural signature.
@@ -238,7 +176,7 @@ function createConversionMixin(Base) {
238
176
  // Evaluate the full type constructor to get resolved fields with defaults.
239
177
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
178
  const typeValue = await this.evaluateTypeConstructor(sigNode);
241
- const resolvedFields = typeValue.structure.type === 'ordered' && typeValue.structure.fields
179
+ const resolvedFields = typeValue.structure.kind === 'ordered' && typeValue.structure.fields
242
180
  ? typeValue.structure.fields
243
181
  : [];
244
182
  const entries = [];
@@ -292,7 +230,7 @@ function createConversionMixin(Base) {
292
230
  // Evaluate the full type constructor to get resolved fields with defaults.
293
231
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
232
  const typeValue = await this.evaluateTypeConstructor(sigNode);
295
- const resolvedFields = typeValue.structure.type === 'dict' && typeValue.structure.fields
233
+ const resolvedFields = typeValue.structure.kind === 'dict' && typeValue.structure.fields
296
234
  ? typeValue.structure.fields
297
235
  : {};
298
236
  const result = {};
@@ -345,7 +283,7 @@ function createConversionMixin(Base) {
345
283
  // Evaluate the full type constructor to get resolved elements with defaults.
346
284
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
347
285
  const typeValue = await this.evaluateTypeConstructor(sigNode);
348
- const resolvedElements = typeValue.structure.type === 'tuple' && typeValue.structure.elements
286
+ const resolvedElements = typeValue.structure.kind === 'tuple' && typeValue.structure.elements
349
287
  ? typeValue.structure.elements
350
288
  : [];
351
289
  const inputEntries = isTupleInput
@@ -379,10 +317,13 @@ function createConversionMixin(Base) {
379
317
  * Returns the value unchanged if the type has no fields or the value type does not match.
380
318
  */
381
319
  hydrateNested(value, fieldType, node) {
382
- if (fieldType.type === 'dict' && fieldType.fields && isDict(value)) {
320
+ if (fieldType.kind === 'dict' &&
321
+ fieldType.fields &&
322
+ isDict(value)) {
323
+ const ft = fieldType;
383
324
  const dictValue = value;
384
325
  const result = {};
385
- for (const [fieldName, resolvedField] of Object.entries(fieldType.fields)) {
326
+ for (const [fieldName, resolvedField] of Object.entries(ft.fields)) {
386
327
  if (fieldName in dictValue) {
387
328
  const fieldValue = this.hydrateNested(dictValue[fieldName], resolvedField.type, node);
388
329
  result[fieldName] = fieldValue;
@@ -401,7 +342,9 @@ function createConversionMixin(Base) {
401
342
  }
402
343
  return result;
403
344
  }
404
- else if (fieldType.type === 'ordered' && fieldType.fields) {
345
+ else if (fieldType.kind === 'ordered' &&
346
+ fieldType.fields) {
347
+ const ft = fieldType;
405
348
  // Only hydrate if the runtime value is an ordered or dict; return unchanged otherwise.
406
349
  if (!isOrdered(value) && !isDict(value)) {
407
350
  return value;
@@ -412,7 +355,7 @@ function createConversionMixin(Base) {
412
355
  ? value.entries.map(([k, v]) => [k, v])
413
356
  : Object.entries(value));
414
357
  const resultEntries = [];
415
- for (const field of fieldType.fields) {
358
+ for (const field of ft.fields) {
416
359
  const name = field.name;
417
360
  if (lookup.has(name)) {
418
361
  const fieldValue = this.hydrateNested(lookup.get(name), field.type, node);
@@ -436,15 +379,17 @@ function createConversionMixin(Base) {
436
379
  }
437
380
  return createOrdered(resultEntries);
438
381
  }
439
- else if (fieldType.type === 'tuple' && fieldType.elements) {
382
+ else if (fieldType.kind === 'tuple' &&
383
+ fieldType.elements) {
384
+ const ft = fieldType;
440
385
  // Only hydrate if the runtime value is a tuple; return unchanged otherwise.
441
386
  if (!isTuple(value)) {
442
387
  return value;
443
388
  }
444
389
  const inputEntries = value.entries;
445
390
  const resultEntries = [];
446
- for (let i = 0; i < fieldType.elements.length; i++) {
447
- const element = fieldType.elements[i];
391
+ for (let i = 0; i < ft.elements.length; i++) {
392
+ const element = ft.elements[i];
448
393
  if (i < inputEntries.length) {
449
394
  const elementValue = this.hydrateNested(inputEntries[i], element.type, node);
450
395
  resultEntries.push(elementValue);
@@ -463,10 +408,6 @@ function createConversionMixin(Base) {
463
408
  }
464
409
  return value;
465
410
  }
466
- /** Throw EC-10 incompatible conversion error. */
467
- throwIncompatible(source, target, node) {
468
- throw new RuntimeError('RILL-R036', `cannot convert ${source} to ${target}`, this.getNodeLocation(node), { source, target });
469
- }
470
411
  };
471
412
  }
472
413
  /**
@@ -282,8 +282,8 @@ function createCoreMixin(Base) {
282
282
  primary.typeName === 'ordered' ||
283
283
  primary.typeName === 'vector' ||
284
284
  primary.typeName === 'type'
285
- ? { type: primary.typeName }
286
- : { type: 'any' },
285
+ ? { kind: primary.typeName }
286
+ : { kind: 'any' },
287
287
  });
288
288
  case 'TypeConstructor':
289
289
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -18,9 +18,17 @@
18
18
  * @internal
19
19
  */
20
20
  import { RuntimeError } from '../../../../types.js';
21
- import { inferType, isTruthy, deepEquals } from '../../values.js';
21
+ import { inferType, isTruthy } from '../../values.js';
22
+ import { BUILT_IN_TYPES } from '../../type-registrations.js';
22
23
  import { createChildContext } from '../../context.js';
23
24
  import { isCallable } from '../../callable.js';
25
+ /**
26
+ * Find the type registration for a value by type name.
27
+ * Returns undefined when no registration matches.
28
+ */
29
+ function findRegistration(typeName) {
30
+ return BUILT_IN_TYPES.find((r) => r.name === typeName);
31
+ }
24
32
  /**
25
33
  * ExpressionsMixin implementation.
26
34
  *
@@ -150,39 +158,39 @@ function createExpressionsMixin(Base) {
150
158
  }
151
159
  }
152
160
  /**
153
- * Evaluate comparison between two values.
154
- * Equality works on all types, ordering requires compatible types.
161
+ * Evaluate comparison between two values via protocol dispatch.
162
+ *
163
+ * - == / != dispatch to protocol.eq; absent eq raises RILL-R002.
164
+ * - Ordering ops dispatch to protocol.compare; absent compare raises RILL-R002.
165
+ *
166
+ * IR-5: Breaking change: bool ordering (e.g. true > false) raises RILL-R002
167
+ * because the bool registration has no protocol.compare.
155
168
  */
156
169
  evaluateBinaryComparison(left, right, op, node) {
170
+ const typeName = inferType(left);
171
+ const reg = findRegistration(typeName);
172
+ if (op === '==' || op === '!=') {
173
+ if (!reg || !reg.protocol.eq) {
174
+ throw new RuntimeError('RILL-R002', `Cannot compare ${typeName} using ${op}`, node.span.start);
175
+ }
176
+ const eqResult = reg.protocol.eq(left, right);
177
+ return op === '==' ? eqResult : !eqResult;
178
+ }
179
+ // Ordering ops: <, >, <=, >=
180
+ const rightTypeName = inferType(right);
181
+ if (!reg || !reg.protocol.compare || typeName !== rightTypeName) {
182
+ throw new RuntimeError('RILL-R002', `Cannot compare ${typeName} with ${rightTypeName} using ${op}`, node.span.start);
183
+ }
184
+ const cmp = reg.protocol.compare(left, right);
157
185
  switch (op) {
158
- case '==':
159
- return deepEquals(left, right);
160
- case '!=':
161
- return !deepEquals(left, right);
162
186
  case '<':
187
+ return cmp < 0;
163
188
  case '>':
189
+ return cmp > 0;
164
190
  case '<=':
191
+ return cmp <= 0;
165
192
  case '>=':
166
- // Ordering comparisons require compatible types
167
- if (typeof left === 'number' && typeof right === 'number') {
168
- return op === '<'
169
- ? left < right
170
- : op === '>'
171
- ? left > right
172
- : op === '<='
173
- ? left <= right
174
- : left >= right;
175
- }
176
- if (typeof left === 'string' && typeof right === 'string') {
177
- return op === '<'
178
- ? left < right
179
- : op === '>'
180
- ? left > right
181
- : op === '<='
182
- ? left <= right
183
- : left >= right;
184
- }
185
- throw new RuntimeError('RILL-R002', `Cannot compare ${inferType(left)} with ${inferType(right)} using ${op}`, node.span.start);
193
+ return cmp >= 0;
186
194
  }
187
195
  }
188
196
  /**
@@ -642,13 +642,13 @@ function createLiteralsMixin(Base) {
642
642
  if (resolvedType === undefined && defaultValue !== undefined) {
643
643
  const defaultKind = typeof defaultValue;
644
644
  if (defaultKind === 'string') {
645
- resolvedType = { type: 'string' };
645
+ resolvedType = { kind: 'string' };
646
646
  }
647
647
  else if (defaultKind === 'number') {
648
- resolvedType = { type: 'number' };
648
+ resolvedType = { kind: 'number' };
649
649
  }
650
650
  else if (defaultKind === 'boolean') {
651
- resolvedType = { type: 'bool' };
651
+ resolvedType = { kind: 'bool' };
652
652
  }
653
653
  }
654
654
  // Evaluate per-param annotations inline