@orcalang/orca-lang 0.1.21 → 0.1.24

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.
@@ -55,7 +55,7 @@ function generateTypeScriptType(type: string, values: string[]): string {
55
55
  if (type === 'enum') {
56
56
  return values.length > 0 ? values.map(v => `'${v}'`).join(' | ') : 'string';
57
57
  }
58
- if (type === 'int_range') {
58
+ if (type === 'int_range' || type === 'decimal_range') {
59
59
  return 'number';
60
60
  }
61
61
  return 'string';
@@ -68,10 +68,12 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
68
68
  return ''; // No condition needed
69
69
 
70
70
  case 'exact':
71
- // For bool type, compare to boolean; for others, compare to string
72
71
  if (condType === 'bool') {
73
72
  return `input.${condName} === ${cell.value}`;
74
73
  }
74
+ if (condType === 'int_range' || condType === 'decimal_range') {
75
+ return `input.${condName} === ${cell.value}`;
76
+ }
75
77
  return `input.${condName} === '${cell.value}'`;
76
78
 
77
79
  case 'negated':
@@ -80,7 +82,7 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
80
82
  }
81
83
  return `input.${condName} !== '${cell.value}'`;
82
84
 
83
- case 'set':
85
+ case 'set': {
84
86
  const checks = cell.values.map(v => {
85
87
  if (condType === 'bool') {
86
88
  return `input.${condName} === ${v}`;
@@ -88,6 +90,16 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
88
90
  return `input.${condName} === '${v}'`;
89
91
  }).join(' || ');
90
92
  return `(${checks})`;
93
+ }
94
+
95
+ case 'compare':
96
+ return `input.${condName} ${cell.op} ${cell.value}`;
97
+
98
+ case 'range': {
99
+ const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
100
+ const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
101
+ return `(${lowCheck} && ${highCheck})`;
102
+ }
91
103
 
92
104
  default:
93
105
  return '';
@@ -246,6 +258,7 @@ export function compileDecisionTableToJSON(dt: DecisionTableDef): string {
246
258
  function generatePythonType(type: string): string {
247
259
  if (type === 'bool') return 'bool';
248
260
  if (type === 'int_range') return 'int';
261
+ if (type === 'decimal_range') return 'float';
249
262
  return 'str';
250
263
  }
251
264
 
@@ -255,6 +268,7 @@ function generatePythonConditionCheck(condName: string, condType: string, cell:
255
268
  return '';
256
269
  case 'exact':
257
270
  if (condType === 'bool') return `input.${condName} == ${cell.value}`;
271
+ if (condType === 'int_range' || condType === 'decimal_range') return `input.${condName} == ${cell.value}`;
258
272
  return `input.${condName} == '${cell.value}'`;
259
273
  case 'negated':
260
274
  if (condType === 'bool') return `input.${condName} != ${cell.value}`;
@@ -265,6 +279,13 @@ function generatePythonConditionCheck(condName: string, condType: string, cell:
265
279
  ).join(' or ');
266
280
  return `(${checks})`;
267
281
  }
282
+ case 'compare':
283
+ return `input.${condName} ${cell.op} ${cell.value}`;
284
+ case 'range': {
285
+ const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
286
+ const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
287
+ return `(${lowCheck} and ${highCheck})`;
288
+ }
268
289
  default:
269
290
  return '';
270
291
  }
@@ -352,6 +373,7 @@ export function compileDecisionTableToPython(dt: DecisionTableDef): string {
352
373
  function generateGoType(type: string): string {
353
374
  if (type === 'bool') return 'bool';
354
375
  if (type === 'int_range') return 'int';
376
+ if (type === 'decimal_range') return 'float64';
355
377
  return 'string';
356
378
  }
357
379
 
@@ -362,6 +384,7 @@ function generateGoConditionCheck(condName: string, condType: string, cell: Cell
362
384
  return '';
363
385
  case 'exact':
364
386
  if (condType === 'bool') return `input.${goField} == ${cell.value}`;
387
+ if (condType === 'int_range' || condType === 'decimal_range') return `input.${goField} == ${cell.value}`;
365
388
  return `input.${goField} == "${cell.value}"`;
366
389
  case 'negated':
367
390
  if (condType === 'bool') return `input.${goField} != ${cell.value}`;
@@ -372,6 +395,13 @@ function generateGoConditionCheck(condName: string, condType: string, cell: Cell
372
395
  ).join(' || ');
373
396
  return `(${checks})`;
374
397
  }
398
+ case 'compare':
399
+ return `input.${goField} ${cell.op} ${cell.value}`;
400
+ case 'range': {
401
+ const lowCheck = cell.lowInc ? `input.${goField} >= ${cell.low}` : `input.${goField} > ${cell.low}`;
402
+ const highCheck = cell.highInc ? `input.${goField} <= ${cell.high}` : `input.${goField} < ${cell.high}`;
403
+ return `(${lowCheck} && ${highCheck})`;
404
+ }
375
405
  default:
376
406
  return '';
377
407
  }
@@ -452,3 +482,121 @@ export function compileDecisionTableToGo(dt: DecisionTableDef): string {
452
482
 
453
483
  return lines.join('\n');
454
484
  }
485
+
486
+ // ============================================================
487
+ // Rust Compiler
488
+ // ============================================================
489
+
490
+ function generateRustType(type: string): string {
491
+ if (type === 'bool') return 'bool';
492
+ if (type === 'int_range') return 'i64';
493
+ if (type === 'decimal_range') return 'f64';
494
+ return 'String';
495
+ }
496
+
497
+ function generateRustConditionCheck(condName: string, condType: string, cell: CellValue): string {
498
+ switch (cell.kind) {
499
+ case 'any':
500
+ return '';
501
+ case 'exact':
502
+ if (condType === 'bool') return `input.${condName} == ${cell.value}`;
503
+ if (condType === 'int_range' || condType === 'decimal_range') return `input.${condName} == ${cell.value}`;
504
+ return `input.${condName} == "${cell.value}"`;
505
+ case 'negated':
506
+ if (condType === 'bool') return `input.${condName} != ${cell.value}`;
507
+ return `input.${condName} != "${cell.value}"`;
508
+ case 'set': {
509
+ const checks = cell.values.map(v =>
510
+ condType === 'bool' ? `input.${condName} == ${v}` : `input.${condName} == "${v}"`
511
+ ).join(' || ');
512
+ return `(${checks})`;
513
+ }
514
+ case 'compare':
515
+ return `input.${condName} ${cell.op} ${cell.value}`;
516
+ case 'range': {
517
+ const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
518
+ const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
519
+ return `(${lowCheck} && ${highCheck})`;
520
+ }
521
+ default:
522
+ return '';
523
+ }
524
+ }
525
+
526
+ export function compileDecisionTableToRust(dt: DecisionTableDef): string {
527
+ const inputTypeName = `${toPascalCase(dt.name)}Input`;
528
+ const outputTypeName = `${toPascalCase(dt.name)}Output`;
529
+ const fnName = `evaluate_${toSnakeCase(dt.name)}`;
530
+
531
+ const lines: string[] = [];
532
+
533
+ lines.push(`/// Input conditions for the ${dt.name} decision table`);
534
+ lines.push('#[derive(Debug, Clone)]');
535
+ lines.push(`pub struct ${inputTypeName} {`);
536
+ for (const cond of dt.conditions) {
537
+ const rustType = generateRustType(cond.type);
538
+ const comment = cond.type === 'enum' && cond.values.length > 0
539
+ ? ` // ${cond.values.join(', ')}`
540
+ : '';
541
+ lines.push(` pub ${cond.name}: ${rustType},${comment}`);
542
+ }
543
+ lines.push('}');
544
+ lines.push('');
545
+
546
+ lines.push(`/// Decision outputs for the ${dt.name} decision table`);
547
+ lines.push('#[derive(Debug, Clone)]');
548
+ lines.push(`pub struct ${outputTypeName} {`);
549
+ for (const action of dt.actions) {
550
+ const rustType = generateRustType(action.type);
551
+ const comment = action.type === 'enum' && action.values && action.values.length > 0
552
+ ? ` // ${action.values.join(', ')}`
553
+ : '';
554
+ lines.push(` pub ${action.name}: ${rustType},${comment}`);
555
+ }
556
+ lines.push('}');
557
+ lines.push('');
558
+
559
+ const policy = dt.policy ?? 'first-match';
560
+ lines.push(`/// Evaluate the ${dt.name} decision table (${policy} policy)`);
561
+ lines.push(`pub fn ${fnName}(input: &${inputTypeName}) -> Option<${outputTypeName}> {`);
562
+
563
+ for (let ruleIdx = 0; ruleIdx < dt.rules.length; ruleIdx++) {
564
+ const rule = dt.rules[ruleIdx];
565
+ const ruleNum = rule.number ?? ruleIdx + 1;
566
+
567
+ const checks: string[] = [];
568
+ for (const [condName, cell] of rule.conditions) {
569
+ const condDef = dt.conditions.find(c => c.name === condName);
570
+ const condType = condDef?.type ?? 'string';
571
+ const check = generateRustConditionCheck(condName, condType, cell);
572
+ if (check) checks.push(check);
573
+ }
574
+
575
+ const actionFields = dt.actions
576
+ .map(a => {
577
+ const value = rule.actions.get(a.name);
578
+ if (value === undefined) return null;
579
+ const aType = a.type as string;
580
+ if (aType === 'bool') return `${a.name}: ${value}`;
581
+ if (aType === 'int_range') return `${a.name}: ${value}`;
582
+ if (aType === 'decimal_range') return `${a.name}: ${value}`;
583
+ return `${a.name}: "${value}".to_string()`;
584
+ })
585
+ .filter((v): v is string => v !== null)
586
+ .join(', ');
587
+
588
+ lines.push(` // Rule ${ruleNum}`);
589
+ if (checks.length === 0) {
590
+ lines.push(` return Some(${outputTypeName} { ${actionFields} });`);
591
+ } else {
592
+ lines.push(` if ${checks.join(' && ')} {`);
593
+ lines.push(` return Some(${outputTypeName} { ${actionFields} });`);
594
+ lines.push(` }`);
595
+ }
596
+ }
597
+
598
+ lines.push(' None // no rule matched');
599
+ lines.push('}');
600
+
601
+ return lines.join('\n');
602
+ }
@@ -186,6 +186,85 @@ async function runHealthCheck(): Promise<HealthReport> {
186
186
  }
187
187
  }
188
188
 
189
+ // ── Step 7: runtime-rust tests ──────────────────────────────────
190
+ {
191
+ const step: StepResult = { name: 'runtime-rust:test', status: 'pending', output: '', duration: 0 };
192
+ const start = Date.now();
193
+ console.log('━━━ Running runtime-rust tests ━━━');
194
+ const result = runCommand('cd packages/runtime-rust && cargo test 2>&1', REPO_ROOT);
195
+ step.duration = Date.now() - start;
196
+ step.status = result.status === 0 ? 'success' : 'failed';
197
+ step.output = result.status === 0 ? 'Tests passed' : result.stdout;
198
+ report.steps.push(step);
199
+ console.log(` ${step.status === 'success' ? '✓' : '✗'} runtime-rust tests ${step.status} (${step.duration}ms)\n`);
200
+ if (step.status === 'failed') {
201
+ report.endTime = Date.now();
202
+ return report;
203
+ }
204
+ }
205
+
206
+ // ── Step 7b: demo-fortran ─────────────────────────────────────
207
+ {
208
+ const step: StepResult = { name: 'demo-fortran', status: 'pending', output: '', duration: 0 };
209
+ const start = Date.now();
210
+
211
+ // Check if gfortran is available
212
+ const gfortranCheck = runCommand('which gfortran 2>/dev/null', REPO_ROOT);
213
+ if (gfortranCheck.status !== 0) {
214
+ step.duration = Date.now() - start;
215
+ step.status = 'success';
216
+ step.output = 'Skipped (gfortran not installed)';
217
+ report.steps.push(step);
218
+ console.log(` ○ demo-fortran skipped (gfortran not installed) (${step.duration}ms)\n`);
219
+ } else {
220
+ console.log('━━━ Building and running demo-fortran ━━━');
221
+ const result = runCommand('cd packages/demo-fortran && make run 2>&1', REPO_ROOT);
222
+ step.duration = Date.now() - start;
223
+ step.status = result.status === 0 ? 'success' : 'failed';
224
+ step.output = result.status === 0 ? 'Demo passed' : result.stdout;
225
+ report.steps.push(step);
226
+ console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-fortran ${step.status} (${step.duration}ms)\n`);
227
+ if (step.status === 'failed') {
228
+ report.endTime = Date.now();
229
+ return report;
230
+ }
231
+ }
232
+ }
233
+
234
+ // ── Step 7c: demo-rust ─────────────────────────────────────────
235
+ {
236
+ const step: StepResult = { name: 'demo-rust', status: 'pending', output: '', duration: 0 };
237
+ const start = Date.now();
238
+ console.log('━━━ Running demo-rust tests ━━━');
239
+ const result = runCommand('cd packages/demo-rust && cargo test 2>&1', REPO_ROOT);
240
+ step.duration = Date.now() - start;
241
+ step.status = result.status === 0 ? 'success' : 'failed';
242
+ step.output = result.status === 0 ? 'Tests passed' : result.stdout;
243
+ report.steps.push(step);
244
+ console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-rust tests ${step.status} (${step.duration}ms)\n`);
245
+ if (step.status === 'failed') {
246
+ report.endTime = Date.now();
247
+ return report;
248
+ }
249
+ }
250
+
251
+ // ── Step 7d: demo-rust-event ───────────────────────────────────
252
+ {
253
+ const step: StepResult = { name: 'demo-rust-event', status: 'pending', output: '', duration: 0 };
254
+ const start = Date.now();
255
+ console.log('━━━ Running demo-rust-event ━━━');
256
+ const result = runCommand('cd packages/demo-rust-event && cargo run 2>&1', REPO_ROOT);
257
+ step.duration = Date.now() - start;
258
+ step.status = result.status === 0 ? 'success' : 'failed';
259
+ step.output = result.status === 0 ? 'Demo passed' : result.stdout;
260
+ report.steps.push(step);
261
+ console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-rust-event ${step.status} (${step.duration}ms)\n`);
262
+ if (step.status === 'failed') {
263
+ report.endTime = Date.now();
264
+ return report;
265
+ }
266
+ }
267
+
189
268
  // ── Step 8: demo-nanolab tests ─────────────────────────────────
190
269
  {
191
270
  const step: StepResult = { name: 'demo-nanolab:test', status: 'pending', output: '', duration: 0 };
@@ -1,6 +1,6 @@
1
1
  // Decision Table AST Types
2
2
 
3
- export type ConditionType = 'bool' | 'enum' | 'int_range' | 'string';
3
+ export type ConditionType = 'bool' | 'enum' | 'int_range' | 'decimal_range' | 'string';
4
4
 
5
5
  export interface ConditionDef {
6
6
  name: string;
@@ -22,7 +22,9 @@ export type CellValue =
22
22
  | { kind: 'any' } // "-" wildcard
23
23
  | { kind: 'exact'; value: string } // exact match
24
24
  | { kind: 'negated'; value: string } // "!value"
25
- | { kind: 'set'; values: string[] }; // "a,b" (match any in set)
25
+ | { kind: 'set'; values: string[] } // "a,b" (match any in set)
26
+ | { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: number } // >=750, <0.3
27
+ | { kind: 'range'; low: number; high: number; lowInc: boolean; highInc: boolean }; // 700..749, 0.3-0.4
26
28
 
27
29
  export interface Rule {
28
30
  number?: number; // optional rule # from the # column
@@ -21,7 +21,37 @@ function findColumnIndex(headers: string[], name: string): number {
21
21
 
22
22
  // --- Cell Value Parsing ---
23
23
 
24
- function parseCellValue(text: string | undefined): CellValue {
24
+ function parseNumericCell(text: string): CellValue | null {
25
+ // Suffix form: 750+ means >=750
26
+ const suffixPlus = text.match(/^(-?\d+(?:\.\d+)?)\+$/);
27
+ if (suffixPlus) {
28
+ return { kind: 'compare', op: '>=', value: parseFloat(suffixPlus[1]) };
29
+ }
30
+
31
+ // Comparison operators: >=750, <=0.3, >100, <600
32
+ const cmpMatch = text.match(/^(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)$/);
33
+ if (cmpMatch) {
34
+ return { kind: 'compare', op: cmpMatch[1] as '>' | '>=' | '<' | '<=', value: parseFloat(cmpMatch[2]) };
35
+ }
36
+
37
+ // Range with .. separator: 1..50 (inclusive both ends)
38
+ const dotRange = text.match(/^(-?\d+(?:\.\d+)?)\s*\.\.\s*(-?\d+(?:\.\d+)?)$/);
39
+ if (dotRange) {
40
+ return { kind: 'range', low: parseFloat(dotRange[1]), high: parseFloat(dotRange[2]), lowInc: true, highInc: true };
41
+ }
42
+
43
+ // Range with - separator for numbers: 700-749, 0.3-0.4
44
+ // Must have at least one side that looks numeric (digit-starting).
45
+ // Negative numbers on the left are ambiguous with subtraction — require left >= 0.
46
+ const dashRange = text.match(/^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)$/);
47
+ if (dashRange) {
48
+ return { kind: 'range', low: parseFloat(dashRange[1]), high: parseFloat(dashRange[2]), lowInc: true, highInc: true };
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function parseCellValue(text: string | undefined, condType?: ConditionType): CellValue {
25
55
  if (!text || text.trim() === '' || text.trim() === '-') {
26
56
  return { kind: 'any' };
27
57
  }
@@ -36,6 +66,12 @@ function parseCellValue(text: string | undefined): CellValue {
36
66
  }
37
67
  }
38
68
 
69
+ // Numeric range/compare patterns — only attempt for numeric condition types
70
+ if (condType === 'int_range' || condType === 'decimal_range') {
71
+ const numeric = parseNumericCell(trimmed);
72
+ if (numeric) return numeric;
73
+ }
74
+
39
75
  // Set: a,b,c
40
76
  if (trimmed.includes(',')) {
41
77
  return { kind: 'set', values: trimmed.split(',').map(v => v.trim()).filter(Boolean) };
@@ -65,11 +101,11 @@ function parseConditionsTable(table: MdTable): ConditionDef[] {
65
101
 
66
102
  if (type === 'bool') {
67
103
  values = valuesStr.trim() ? valuesStr.split(',').map(v => v.trim()) : ['true', 'false'];
68
- } else if (type === 'int_range') {
69
- // Parse min..max format
70
- const rangeMatch = valuesStr.match(/(\d+)\s*\.\.\s*(\d+)/);
104
+ } else if (type === 'int_range' || type === 'decimal_range') {
105
+ // Parse min..max format (integers or decimals)
106
+ const rangeMatch = valuesStr.match(/(-?\d+(?:\.\d+)?)\s*\.\.\s*(-?\d+(?:\.\d+)?)/);
71
107
  if (rangeMatch) {
72
- range = { min: parseInt(rangeMatch[1], 10), max: parseInt(rangeMatch[2], 10) };
108
+ range = { min: parseFloat(rangeMatch[1]), max: parseFloat(rangeMatch[2]) };
73
109
  values = [];
74
110
  } else {
75
111
  values = [];
@@ -116,7 +152,8 @@ function parseActionsTable(table: MdTable): ActionOutputDef[] {
116
152
  function parseRulesTable(
117
153
  table: MdTable,
118
154
  conditionNames: Set<string>,
119
- actionNames: Set<string>
155
+ actionNames: Set<string>,
156
+ conditionDefs?: ConditionDef[]
120
157
  ): { rules: Rule[]; warnings: string[] } {
121
158
  const warnings: string[] = [];
122
159
  const rules: Rule[] = [];
@@ -167,7 +204,8 @@ function parseRulesTable(
167
204
  rule.number = num;
168
205
  }
169
206
  } else if (col.type === 'condition') {
170
- const cellValue = parseCellValue(cell);
207
+ const condDef = conditionDefs?.find(c => c.name === col.name);
208
+ const cellValue = parseCellValue(cell, condDef?.type);
171
209
  rule.conditions.set(col.name, cellValue);
172
210
  } else if (col.type === 'action') {
173
211
  const value = cell?.trim() || '';
@@ -254,7 +292,7 @@ export function parseDecisionTable(elements: MdElement[]): { decisionTable: Deci
254
292
  } else if (currentSection === 'actions') {
255
293
  actions = parseActionsTable(el);
256
294
  } else if (currentSection === 'rules') {
257
- const result = parseRulesTable(el, new Set(conditions.map(c => c.name)), new Set(actions.map(a => a.name)));
295
+ const result = parseRulesTable(el, new Set(conditions.map(c => c.name)), new Set(actions.map(a => a.name)), conditions);
258
296
  rules = result.rules;
259
297
  warnings.push(...result.warnings);
260
298
  }