@mica-js/ddl 0.0.0-stage → 0.2.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.
@@ -0,0 +1,1423 @@
1
+ import { BuildError, validateDeviceIR, } from '@mica-js/device-model';
2
+ const numberType = { kind: 'number' };
3
+ const boolType = { kind: 'bool' };
4
+ const stringType = { kind: 'string' };
5
+ const intentType = { kind: 'intent' };
6
+ const bytesType = { kind: 'bytes' };
7
+ const numeric = new Set([
8
+ 'isNaN',
9
+ 'isFinite',
10
+ 'isInteger',
11
+ 'isSafeInteger',
12
+ 'abs',
13
+ 'floor',
14
+ 'ceil',
15
+ 'trunc',
16
+ 'round',
17
+ 'sqrt',
18
+ 'min',
19
+ 'max',
20
+ 'pow',
21
+ ]);
22
+ const enumMembers = {
23
+ Logic: ['low', 'high', 'unknown'],
24
+ SampleStatus: ['valid', 'floating'],
25
+ Overflow: ['drop_new', 'drop_oldest', 'error'],
26
+ };
27
+ const reserved = new Set([
28
+ 'device',
29
+ 'default',
30
+ 'const',
31
+ 'let',
32
+ 'enum',
33
+ 'record',
34
+ 'state',
35
+ 'derived',
36
+ 'pin',
37
+ 'memory',
38
+ 'fn',
39
+ 'pure',
40
+ 'process',
41
+ 'on',
42
+ 'reaction',
43
+ 'as',
44
+ 'capture',
45
+ 'if',
46
+ 'else',
47
+ 'match',
48
+ 'while',
49
+ 'loop',
50
+ 'break',
51
+ 'continue',
52
+ 'return',
53
+ 'await',
54
+ 'assert',
55
+ 'redirect',
56
+ 'true',
57
+ 'false',
58
+ 'none',
59
+ 'number',
60
+ 'bool',
61
+ 'string',
62
+ 'bytes',
63
+ 'intent',
64
+ 'sample',
65
+ 'NaN',
66
+ 'Infinity',
67
+ ...Object.keys(enumMembers),
68
+ ...numeric,
69
+ 'load8',
70
+ 'load16le',
71
+ 'load16be',
72
+ 'load32le',
73
+ 'load32be',
74
+ 'store8',
75
+ 'store16le',
76
+ 'store16be',
77
+ 'store32le',
78
+ 'store32be',
79
+ 'copyBytes',
80
+ 'resistor',
81
+ 'random',
82
+ 'now',
83
+ 'ticks',
84
+ ]);
85
+ const binaryOperators = {
86
+ '+': 'add',
87
+ '-': 'subtract',
88
+ '*': 'multiply',
89
+ '/': 'divide',
90
+ '%': 'remainder',
91
+ '**': 'power',
92
+ '<': 'lessThan',
93
+ '<=': 'lessThanOrEqual',
94
+ '>': 'greaterThan',
95
+ '>=': 'greaterThanOrEqual',
96
+ '==': 'equal',
97
+ '!=': 'notEqual',
98
+ '&': 'bitAnd',
99
+ '|': 'bitOr',
100
+ '^': 'bitXor',
101
+ '<<': 'shiftLeft',
102
+ '>>': 'shiftRight',
103
+ '>>>': 'shiftRightUnsigned',
104
+ };
105
+ function isValueType(type) {
106
+ return type.kind !== 'pin' && type.kind !== 'memory';
107
+ }
108
+ function same(a, b) {
109
+ return (a.kind === b.kind &&
110
+ (a.kind === 'optional' && b.kind === 'optional'
111
+ ? same(a.inner, b.inner)
112
+ : (a.kind === 'enum' || a.kind === 'record') && (b.kind === 'enum' || b.kind === 'record')
113
+ ? a.name === b.name
114
+ : true));
115
+ }
116
+ function base(type) {
117
+ return type.kind === 'optional' ? type.inner : type;
118
+ }
119
+ function optional(type) {
120
+ return type.kind === 'optional' ? type : { kind: 'optional', inner: type };
121
+ }
122
+ function intersection(paths) {
123
+ const result = new Map(paths[0]);
124
+ for (const [name, fact] of result)
125
+ if (paths.some((path) => path.get(name) !== fact))
126
+ result.delete(name);
127
+ return result;
128
+ }
129
+ function isNone(node) {
130
+ return node.$type === 'Literal' && node.value === 'none';
131
+ }
132
+ /** Lowers Langium's structured AST directly into the canonical operation regions. */
133
+ export class DDLCompiler {
134
+ device;
135
+ uri;
136
+ symbols = new Map();
137
+ types = new Map();
138
+ resolving = new Set();
139
+ sources = new WeakMap();
140
+ valueTypes = new WeakMap();
141
+ constructor(device, uri) {
142
+ this.device = device;
143
+ this.uri = uri;
144
+ for (const member of [...device.properties, ...device.members]) {
145
+ if (member.$type === 'AssertStmt')
146
+ continue;
147
+ this.checkName(member.name, member);
148
+ if (this.symbols.has(member.name))
149
+ this.fail(member, `Duplicate declaration '${member.name}'.`, 'IR_SCOPE');
150
+ this.symbols.set(member.name, member);
151
+ }
152
+ }
153
+ source(node) {
154
+ const cached = this.sources.get(node);
155
+ if (cached)
156
+ return cached;
157
+ const range = node.$cstNode?.range;
158
+ const source = range
159
+ ? {
160
+ uri: this.uri,
161
+ range: {
162
+ start: { line: range.start.line + 1, column: range.start.character + 1 },
163
+ end: { line: range.end.line + 1, column: range.end.character + 1 },
164
+ },
165
+ }
166
+ : { label: this.device.name };
167
+ this.sources.set(node, source);
168
+ return source;
169
+ }
170
+ fail(node, message, code = 'IR_TYPE') {
171
+ throw new BuildError([
172
+ { code, message, phase: 'finalize', path: [], source: this.source(node), related: [] },
173
+ ]);
174
+ }
175
+ checkName(name, node) {
176
+ if (reserved.has(name))
177
+ this.fail(node, `Name '${name}' is reserved.`, 'IR_SCOPE');
178
+ }
179
+ type(node) {
180
+ const cached = this.valueTypes.get(node);
181
+ if (cached)
182
+ return cached;
183
+ let type;
184
+ switch (node.name) {
185
+ case 'number':
186
+ case 'bool':
187
+ case 'string':
188
+ case 'bytes':
189
+ case 'intent':
190
+ case 'sample':
191
+ type = { kind: node.name };
192
+ break;
193
+ default: {
194
+ if (node.name in enumMembers) {
195
+ type = { kind: 'enum', name: node.name };
196
+ break;
197
+ }
198
+ const decl = this.symbols.get(node.name);
199
+ if (decl?.$type !== 'EnumDecl' && decl?.$type !== 'RecordDecl')
200
+ this.fail(node, `Unknown value type '${node.name}'.`, 'IR_REFERENCE');
201
+ type = { kind: decl.$type === 'EnumDecl' ? 'enum' : 'record', name: node.name };
202
+ }
203
+ }
204
+ if (node.name in enumMembers)
205
+ type = { kind: 'enum', name: node.name };
206
+ const resolved = node.optional ? optional(type) : type;
207
+ this.valueTypes.set(node, resolved);
208
+ return resolved;
209
+ }
210
+ parameterType(node) {
211
+ if (node.value)
212
+ return this.type(node.value);
213
+ return { kind: node.name };
214
+ }
215
+ context(mode, returns) {
216
+ return {
217
+ operations: [],
218
+ bindings: new Map(),
219
+ facts: new Map(),
220
+ counter: { result: 0, local: 0 },
221
+ returns,
222
+ mode,
223
+ loops: [],
224
+ ended: false,
225
+ };
226
+ }
227
+ child(context) {
228
+ return {
229
+ ...context,
230
+ operations: [],
231
+ bindings: new Map(context.bindings),
232
+ facts: new Map(context.facts),
233
+ ended: false,
234
+ };
235
+ }
236
+ emit(context, node, operation) {
237
+ context.operations.push({ ...operation, source: this.source(node) });
238
+ }
239
+ value(context, node, type, operation) {
240
+ const result = context.counter.result++;
241
+ this.emit(context, node, { ...operation, result, type });
242
+ return { id: result, type, ...(operation.kind === 'optional' ? { nonNull: true } : {}) };
243
+ }
244
+ constant(context, node, type, constant) {
245
+ return this.value(context, node, type, { kind: 'constant', constant });
246
+ }
247
+ expect(context, node, value, expected) {
248
+ if (!expected || same(value.type, expected))
249
+ return value;
250
+ if (expected.kind === 'optional' && same(value.type, expected.inner))
251
+ return this.value(context, node, expected, {
252
+ kind: 'optional',
253
+ value: value.id,
254
+ });
255
+ return this.fail(node, `Expected ${this.describe(expected)}, received ${this.describe(value.type)}.`);
256
+ }
257
+ describe(type) {
258
+ return type.kind === 'optional'
259
+ ? `${this.describe(type.inner)}?`
260
+ : type.kind === 'enum' || type.kind === 'record'
261
+ ? type.name
262
+ : type.kind;
263
+ }
264
+ symbolType(name, node) {
265
+ const cached = this.types.get(name);
266
+ if (cached)
267
+ return cached;
268
+ if (this.resolving.has(name))
269
+ this.fail(node, `Cyclic dependency involving '${name}'.`, 'IR_DEPENDENCY');
270
+ const symbol = this.symbols.get(name);
271
+ if (!symbol)
272
+ this.fail(node, `Unknown name '${name}'.`, 'IR_REFERENCE');
273
+ this.resolving.add(name);
274
+ let type;
275
+ switch (symbol.$type) {
276
+ case 'Property':
277
+ case 'StateDecl':
278
+ case 'DerivedDecl':
279
+ case 'ChannelDecl':
280
+ type = this.type(symbol.type);
281
+ break;
282
+ case 'ConstantDecl':
283
+ type = symbol.type
284
+ ? this.type(symbol.type)
285
+ : this.expression(this.context('computation'), symbol.value).type;
286
+ break;
287
+ default:
288
+ return this.fail(node, `'${name}' is not a value.`, 'IR_TYPE');
289
+ }
290
+ this.resolving.delete(name);
291
+ this.types.set(name, type);
292
+ return type;
293
+ }
294
+ bind(context, name, binding, node) {
295
+ this.checkName(name, node);
296
+ if (context.bindings.has(name) || this.symbols.has(name))
297
+ this.fail(node, `Binding '${name}' shadows a visible declaration.`, 'IR_SCOPE');
298
+ context.bindings.set(name, binding);
299
+ }
300
+ resource(context, node, kind) {
301
+ if (node.$type !== 'Reference')
302
+ return this.fail(node, `Expected a direct ${kind} resource reference.`);
303
+ const binding = context.bindings.get(node.name);
304
+ if (binding?.kind === 'parameter' && binding.type.kind === kind)
305
+ return { kind: 'parameter', resource: kind, index: binding.index };
306
+ const decl = this.symbols.get(node.name);
307
+ if (decl?.$type === (kind === 'pin' ? 'PinDecl' : 'MemoryDecl'))
308
+ return kind === 'pin'
309
+ ? { kind: 'pin', name: node.name }
310
+ : { kind: 'memory', name: node.name };
311
+ return this.fail(node, `'${node.name}' is not a ${kind} resource.`);
312
+ }
313
+ resourceKind(context, node) {
314
+ if (node.$type !== 'Reference')
315
+ return undefined;
316
+ const binding = context.bindings.get(node.name);
317
+ if (binding?.type.kind === 'pin' || binding?.type.kind === 'memory')
318
+ return binding.type.kind;
319
+ const decl = this.symbols.get(node.name);
320
+ return decl?.$type === 'PinDecl' ? 'pin' : decl?.$type === 'MemoryDecl' ? 'memory' : undefined;
321
+ }
322
+ read(context, node) {
323
+ const binding = context.bindings.get(node.name);
324
+ if (!binding) {
325
+ const decl = this.symbols.get(node.name);
326
+ const resource = decl?.$type === 'Property'
327
+ ? 'property'
328
+ : decl?.$type === 'ConstantDecl'
329
+ ? 'const'
330
+ : decl?.$type === 'StateDecl'
331
+ ? 'state'
332
+ : decl?.$type === 'DerivedDecl'
333
+ ? 'derived'
334
+ : undefined;
335
+ if (!resource)
336
+ this.fail(node, `Name '${node.name}' is not a readable value.`, 'IR_REFERENCE');
337
+ return this.value(context, node, this.symbolType(node.name, node), {
338
+ kind: 'read',
339
+ resource,
340
+ name: node.name,
341
+ });
342
+ }
343
+ if (!isValueType(binding.type))
344
+ this.fail(node, 'Resources cannot be used as ordinary values.');
345
+ const op = binding.kind === 'local'
346
+ ? { kind: 'localRead', local: binding.index }
347
+ : binding.kind === 'parameter'
348
+ ? { kind: 'parameter', index: binding.index }
349
+ : binding.kind === 'capture'
350
+ ? { kind: 'reactionCapture', index: binding.index }
351
+ : { kind: 'reactionValue', binding: binding.kind };
352
+ const value = this.value(context, node, binding.type, op);
353
+ return binding.type.kind === 'optional' && context.facts.get(node.name) === 'nonNull'
354
+ ? this.value(context, node, binding.type.inner, {
355
+ kind: 'nonNull',
356
+ value: value.id,
357
+ })
358
+ : value;
359
+ }
360
+ number(node, text) {
361
+ const value = Number(text.replaceAll('_', ''));
362
+ if (!Number.isFinite(value))
363
+ this.fail(node, 'A numeric literal must be finite.');
364
+ return value;
365
+ }
366
+ string(node, text) {
367
+ return text
368
+ .slice(1, -1)
369
+ .replace(/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|(["\\nrt]))/g, (_match, point, unit, simple) => {
370
+ if (point) {
371
+ const value = Number.parseInt(point, 16);
372
+ if (value > 0x10ffff)
373
+ this.fail(node, 'Unicode escape is outside the valid code point range.');
374
+ return String.fromCodePoint(value);
375
+ }
376
+ if (unit)
377
+ return String.fromCharCode(Number.parseInt(unit, 16));
378
+ return simple === 'n' ? '\n' : simple === 'r' ? '\r' : simple === 't' ? '\t' : simple;
379
+ });
380
+ }
381
+ fieldType(node, type, field) {
382
+ if (type.kind === 'sample') {
383
+ if (field === 'voltage')
384
+ return optional(numberType);
385
+ if (field === 'level')
386
+ return { kind: 'enum', name: 'Logic' };
387
+ if (field === 'status')
388
+ return { kind: 'enum', name: 'SampleStatus' };
389
+ }
390
+ if ((type.kind === 'bytes' || type.kind === 'string') && field === 'length')
391
+ return numberType;
392
+ if (type.kind === 'record') {
393
+ const decl = this.symbols.get(type.name);
394
+ const match = decl?.$type === 'RecordDecl'
395
+ ? decl.fields.find((candidate) => candidate.name === field)
396
+ : undefined;
397
+ if (match)
398
+ return { kind: match.type };
399
+ }
400
+ return this.fail(node, `Unknown field '${field}' on ${this.describe(type)}.`);
401
+ }
402
+ expression(context, node, expected) {
403
+ let value;
404
+ switch (node.$type) {
405
+ case 'NumberLiteral':
406
+ value = this.constant(context, node, numberType, {
407
+ kind: 'number',
408
+ value: this.number(node, node.value),
409
+ });
410
+ break;
411
+ case 'StringLiteral':
412
+ value = this.constant(context, node, stringType, {
413
+ kind: 'string',
414
+ value: this.string(node, node.value),
415
+ });
416
+ break;
417
+ case 'Literal': {
418
+ if (node.value === 'none') {
419
+ if (expected?.kind !== 'optional')
420
+ this.fail(node, 'The none literal requires an optional target type.');
421
+ value = this.constant(context, node, expected, { kind: 'none' });
422
+ }
423
+ else if (node.value === 'true' || node.value === 'false')
424
+ value = this.constant(context, node, boolType, {
425
+ kind: 'bool',
426
+ value: node.value === 'true',
427
+ });
428
+ else
429
+ value = this.constant(context, node, numberType, {
430
+ kind: 'number',
431
+ value: node.value === 'NaN' ? NaN : Infinity,
432
+ });
433
+ break;
434
+ }
435
+ case 'Reference':
436
+ value = this.read(context, node);
437
+ break;
438
+ case 'Unary': {
439
+ const type = node.op === '!' ? boolType : numberType;
440
+ const input = this.expression(context, node.value, type);
441
+ const operator = node.op === '+'
442
+ ? 'positive'
443
+ : node.op === '-'
444
+ ? 'negate'
445
+ : node.op === '!'
446
+ ? 'not'
447
+ : 'bitNot';
448
+ value = this.value(context, node, type, {
449
+ kind: 'unary',
450
+ operator,
451
+ value: input.id,
452
+ });
453
+ break;
454
+ }
455
+ case 'Binary':
456
+ value = this.binary(context, node);
457
+ break;
458
+ case 'Select':
459
+ value = this.select(context, node, expected);
460
+ break;
461
+ case 'BytesLiteral':
462
+ value = this.value(context, node, bytesType, {
463
+ kind: 'bytes',
464
+ elements: node.elements.map((element) => this.expression(context, element, numberType).id),
465
+ });
466
+ break;
467
+ case 'IntentLiteral':
468
+ value = this.value(context, node, intentType, {
469
+ kind: 'combineIntents',
470
+ values: node.elements.map((element) => this.expression(context, element, intentType).id),
471
+ });
472
+ break;
473
+ case 'RecordLiteral': {
474
+ const type = expected && base(expected);
475
+ if (type?.kind !== 'record')
476
+ this.fail(node, 'A record literal requires a named record target type.');
477
+ const decl = this.symbols.get(type.name);
478
+ if (decl?.$type !== 'RecordDecl')
479
+ this.fail(node, `Unknown record '${type.name}'.`);
480
+ const seen = new Set();
481
+ const fields = node.fields.map((field) => {
482
+ const target = decl.fields.find((candidate) => candidate.name === field.name);
483
+ if (!target || seen.has(field.name))
484
+ this.fail(field, `Unknown or duplicate record field '${field.name}'.`);
485
+ seen.add(field.name);
486
+ return {
487
+ name: field.name,
488
+ value: this.expression(context, field.value, { kind: target.type }).id,
489
+ };
490
+ });
491
+ if (seen.size !== decl.fields.length)
492
+ this.fail(node, 'Record literal is missing required fields.');
493
+ value = this.value(context, node, type, { kind: 'record', fields });
494
+ break;
495
+ }
496
+ case 'MemberAccess': {
497
+ if (node.object.$type === 'Reference') {
498
+ const root = node.object.name;
499
+ const decl = this.symbols.get(root);
500
+ const members = enumMembers[root] ?? (decl?.$type === 'EnumDecl' ? decl.members : undefined);
501
+ if (members) {
502
+ if (!members.includes(node.member))
503
+ this.fail(node, `Unknown enum member '${root}.${node.member}'.`, 'IR_REFERENCE');
504
+ value = this.constant(context, node, { kind: 'enum', name: root }, { kind: 'enum', name: root, member: node.member });
505
+ break;
506
+ }
507
+ const resource = this.resourceKind(context, node.object);
508
+ if (resource === 'pin' && node.member === 'sample') {
509
+ value = this.value(context, node, { kind: 'sample' }, {
510
+ kind: 'readSample',
511
+ pin: this.resource(context, node.object, 'pin'),
512
+ });
513
+ break;
514
+ }
515
+ if (resource === 'memory' && node.member === 'length') {
516
+ value = this.value(context, node, numberType, {
517
+ kind: 'memoryLength',
518
+ memory: this.resource(context, node.object, 'memory'),
519
+ });
520
+ break;
521
+ }
522
+ }
523
+ const object = this.expression(context, node.object);
524
+ const type = this.fieldType(node, object.type, node.member);
525
+ value = this.value(context, node, type, node.member === 'length' &&
526
+ (object.type.kind === 'bytes' || object.type.kind === 'string')
527
+ ? { kind: 'length', value: object.id }
528
+ : { kind: 'field', value: object.id, field: node.member });
529
+ const key = this.factKey(context, node);
530
+ if (type.kind === 'optional' && key && context.facts.get(key) === 'nonNull')
531
+ value = this.value(context, node, type.inner, { kind: 'nonNull', value: value.id });
532
+ break;
533
+ }
534
+ case 'IndexAccess': {
535
+ const object = this.expression(context, node.object);
536
+ if (object.type.kind !== 'bytes' && object.type.kind !== 'string')
537
+ this.fail(node, 'Only string and bytes values can be indexed.');
538
+ const index = this.expression(context, node.index, numberType);
539
+ value = this.value(context, node, object.type.kind === 'bytes' ? numberType : stringType, {
540
+ kind: 'index',
541
+ value: object.id,
542
+ index: index.id,
543
+ });
544
+ break;
545
+ }
546
+ case 'Call': {
547
+ const result = this.call(context, node, false);
548
+ if (!result)
549
+ this.fail(node, 'A void call cannot be used as a value.');
550
+ value = result;
551
+ break;
552
+ }
553
+ default:
554
+ return this.fail(node, 'Unsupported expression.', 'IR_SCHEMA');
555
+ }
556
+ return this.expect(context, node, value, expected);
557
+ }
558
+ infer(context, node, expected) {
559
+ return this.expression(this.child(context), node, expected).type;
560
+ }
561
+ binary(context, node) {
562
+ const op = node.op;
563
+ if (op === '&&' || op === '||') {
564
+ const left = this.expression(context, node.left, boolType);
565
+ const branch = this.child(context);
566
+ branch.facts = this.facts(node.left, op === '&&', context);
567
+ const right = this.expression(branch, node.right, boolType);
568
+ this.emit(branch, node.right, { kind: 'yield', value: right.id });
569
+ return this.value(context, node, boolType, {
570
+ kind: op === '&&' ? 'and' : 'or',
571
+ left: left.id,
572
+ right: {
573
+ type: boolType,
574
+ body: { operations: branch.operations, source: this.source(node.right) },
575
+ },
576
+ });
577
+ }
578
+ if (op === '==' || op === '!=') {
579
+ let equal;
580
+ const leftResource = this.resourceKind(context, node.left);
581
+ const rightResource = this.resourceKind(context, node.right);
582
+ if (leftResource || rightResource) {
583
+ if (!leftResource || leftResource !== rightResource)
584
+ this.fail(node, 'Resource identity comparison requires matching resource kinds.');
585
+ const left = leftResource === 'pin'
586
+ ? this.resource(context, node.left, 'pin')
587
+ : this.resource(context, node.left, 'memory');
588
+ const right = rightResource === 'pin'
589
+ ? this.resource(context, node.right, 'pin')
590
+ : this.resource(context, node.right, 'memory');
591
+ equal = this.value(context, node, boolType, {
592
+ kind: 'refEqual',
593
+ left,
594
+ right,
595
+ });
596
+ return op === '=='
597
+ ? equal
598
+ : this.value(context, node, boolType, {
599
+ kind: 'unary',
600
+ operator: 'not',
601
+ value: equal.id,
602
+ });
603
+ }
604
+ if (isNone(node.left) || isNone(node.right)) {
605
+ if (isNone(node.left) && isNone(node.right))
606
+ equal = this.constant(context, node, boolType, { kind: 'bool', value: true });
607
+ else {
608
+ const input = this.expression(context, isNone(node.left) ? node.right : node.left);
609
+ if (input.type.kind !== 'optional') {
610
+ const original = isNone(node.left) ? node.right : node.left;
611
+ const key = this.factKey(context, original);
612
+ if (!key ||
613
+ (context.facts.get(key) !== 'nonNull' &&
614
+ (original.$type !== 'Reference' ||
615
+ context.bindings.get(original.name)?.type.kind !== 'optional')))
616
+ this.fail(node, 'Only optional values can be compared with none.');
617
+ equal = this.constant(context, node, boolType, { kind: 'bool', value: false });
618
+ }
619
+ else
620
+ equal = this.value(context, node, boolType, {
621
+ kind: 'isNone',
622
+ value: input.id,
623
+ });
624
+ }
625
+ return op === '=='
626
+ ? equal
627
+ : this.value(context, node, boolType, {
628
+ kind: 'unary',
629
+ operator: 'not',
630
+ value: equal.id,
631
+ });
632
+ }
633
+ }
634
+ let left = this.expression(context, node.left);
635
+ let right = this.expression(context, node.right, node.right.$type === 'RecordLiteral' ? left.type : undefined);
636
+ if (op === '==' || op === '!=') {
637
+ if (left.type.kind !== 'optional' && right.type.kind === 'optional')
638
+ left = this.expect(context, node.left, left, right.type);
639
+ else if (left.type.kind === 'optional' && right.type.kind !== 'optional')
640
+ right = this.expect(context, node.right, right, left.type);
641
+ }
642
+ let type = numberType;
643
+ let operator = binaryOperators[op];
644
+ if (op === '==' || op === '!=') {
645
+ if (!same(left.type, right.type) || base(left.type).kind === 'intent')
646
+ this.fail(node, 'Equality requires comparable values of the same type.');
647
+ type = boolType;
648
+ }
649
+ else if (op === '+' && left.type.kind === 'string' && right.type.kind === 'string') {
650
+ type = stringType;
651
+ operator = 'concat';
652
+ }
653
+ else {
654
+ left = this.expect(context, node.left, left, numberType);
655
+ right = this.expect(context, node.right, right, numberType);
656
+ if (['<', '<=', '>', '>='].includes(op))
657
+ type = boolType;
658
+ }
659
+ return this.value(context, node, type, {
660
+ kind: 'binary',
661
+ operator,
662
+ left: left.id,
663
+ right: right.id,
664
+ });
665
+ }
666
+ select(context, node, expected) {
667
+ const condition = this.expression(context, node.condition, boolType);
668
+ const yes = this.child(context), no = this.child(context);
669
+ yes.facts = this.facts(node.condition, true, context);
670
+ no.facts = this.facts(node.condition, false, context);
671
+ let type = expected;
672
+ if (!type) {
673
+ if (isNone(node.yes) && isNone(node.no))
674
+ this.fail(node, 'Conditional none values require an optional target type.');
675
+ const yesType = isNone(node.yes) ? undefined : this.infer(yes, node.yes);
676
+ const noType = isNone(node.no) ? undefined : this.infer(no, node.no);
677
+ if (!yesType || !noType)
678
+ type = optional((yesType ?? noType));
679
+ else if (same(yesType, noType))
680
+ type = yesType;
681
+ else if (same(base(yesType), base(noType)))
682
+ type = optional(base(yesType));
683
+ else
684
+ this.fail(node, 'Conditional branches have incompatible types.');
685
+ }
686
+ const y = this.expression(yes, node.yes, type), n = this.expression(no, node.no, type);
687
+ this.emit(yes, node.yes, { kind: 'yield', value: y.id });
688
+ this.emit(no, node.no, { kind: 'yield', value: n.id });
689
+ const value = this.value(context, node, type, {
690
+ kind: 'select',
691
+ condition: condition.id,
692
+ yes: { type, body: { operations: yes.operations, source: this.source(node.yes) } },
693
+ no: { type, body: { operations: no.operations, source: this.source(node.no) } },
694
+ });
695
+ if ((y.type.kind !== 'optional' || y.nonNull) && (n.type.kind !== 'optional' || n.nonNull))
696
+ value.nonNull = true;
697
+ return value;
698
+ }
699
+ args(context, call, names, types) {
700
+ if (call.args.length !== names.length)
701
+ this.fail(call, `Expected ${names.length} arguments, received ${call.args.length}.`);
702
+ const named = call.args.some((arg) => arg.$type === 'NamedArgument');
703
+ if (named && call.args.some((arg) => arg.$type !== 'NamedArgument'))
704
+ this.fail(call, 'Named and positional arguments cannot be mixed.');
705
+ const args = new Array(names.length);
706
+ for (let i = 0; i < call.args.length; i++) {
707
+ const arg = call.args[i];
708
+ const index = arg.$type === 'NamedArgument' ? names.indexOf(arg.name) : i;
709
+ if (index < 0 || args[index])
710
+ this.fail(arg, 'Unknown or duplicate named argument.');
711
+ const type = types[index];
712
+ args[index] = isValueType(type)
713
+ ? { kind: 'value', value: this.expression(context, arg.value, type).id }
714
+ : type.kind === 'pin'
715
+ ? this.resource(context, arg.value, 'pin')
716
+ : this.resource(context, arg.value, 'memory');
717
+ }
718
+ return args;
719
+ }
720
+ call(context, node, discard) {
721
+ if (node.callee.$type === 'MemberAccess') {
722
+ const callee = node.callee;
723
+ if (callee.object.$type !== 'Reference')
724
+ this.fail(node, 'Only declared endpoint methods may be called.');
725
+ const endpoint = this.symbols.get(callee.object.name);
726
+ if (callee.member === 'send' &&
727
+ endpoint?.$type === 'ChannelDecl' &&
728
+ endpoint.direction === 'output') {
729
+ const args = this.args(context, node, ['value'], [this.type(endpoint.type)]);
730
+ this.emit(context, node, {
731
+ kind: 'send',
732
+ channel: endpoint.name,
733
+ value: args[0].value,
734
+ });
735
+ return undefined;
736
+ }
737
+ if (callee.member === 'notify' &&
738
+ endpoint?.$type === 'NotificationDecl' &&
739
+ endpoint.direction === 'output') {
740
+ this.args(context, node, [], []);
741
+ this.emit(context, node, { kind: 'notify', notification: endpoint.name });
742
+ return undefined;
743
+ }
744
+ return this.fail(node, 'Invalid endpoint operation or direction.', 'IR_EFFECT');
745
+ }
746
+ if (node.callee.$type !== 'Reference')
747
+ this.fail(node, 'Dynamic calls are not supported.');
748
+ const name = node.callee.name;
749
+ if (numeric.has(name)) {
750
+ const names = name === 'min' || name === 'max'
751
+ ? ['left', 'right']
752
+ : name === 'pow'
753
+ ? ['base', 'exponent']
754
+ : ['value'];
755
+ const args = this.args(context, node, names, names.map(() => numberType));
756
+ return this.value(context, node, name.startsWith('is') ? boolType : numberType, {
757
+ kind: 'numeric',
758
+ builtin: name,
759
+ args: args.map((arg) => arg.value),
760
+ });
761
+ }
762
+ if (name === 'now' || name === 'random') {
763
+ this.args(context, node, [], []);
764
+ return this.value(context, node, numberType, { kind: name });
765
+ }
766
+ if (/^(load|store)(8|16le|16be|32le|32be)$/.test(name) || name === 'copyBytes') {
767
+ const store = name.startsWith('store');
768
+ const names = store
769
+ ? ['memory', 'offset', 'value']
770
+ : name === 'copyBytes'
771
+ ? ['memory', 'offset', 'length']
772
+ : ['memory', 'offset'];
773
+ const args = this.args(context, node, names, names.map((_name, i) => (i === 0 ? { kind: 'memory' } : numberType)));
774
+ const memory = args[0];
775
+ const offset = args[1].value;
776
+ if (store) {
777
+ this.emit(context, node, {
778
+ kind: name,
779
+ memory,
780
+ offset,
781
+ value: args[2].value,
782
+ });
783
+ return undefined;
784
+ }
785
+ if (name === 'copyBytes')
786
+ return this.value(context, node, bytesType, {
787
+ kind: 'copyBytes',
788
+ memory,
789
+ offset,
790
+ length: args[2].value,
791
+ });
792
+ return this.value(context, node, numberType, {
793
+ kind: name,
794
+ memory,
795
+ offset,
796
+ });
797
+ }
798
+ if (name === 'resistor') {
799
+ const args = this.args(context, node, ['to', 'resistance'], [{ kind: 'pin' }, numberType]);
800
+ return this.value(context, node, intentType, {
801
+ kind: 'resistor',
802
+ to: args[0],
803
+ resistance: args[1].value,
804
+ });
805
+ }
806
+ const target = this.symbols.get(name);
807
+ if (target?.$type !== 'FunctionDecl')
808
+ this.fail(node, `Unknown synchronous function '${name}'.`, 'IR_REFERENCE');
809
+ const args = this.args(context, node, target.params.map((param) => param.name), target.params.map((param) => this.parameterType(param.type)));
810
+ if (!target.returns || discard) {
811
+ this.emit(context, node, { kind: 'call', name, pure: target.pure, args });
812
+ return undefined;
813
+ }
814
+ return this.value(context, node, this.type(target.returns), {
815
+ kind: 'callValue',
816
+ name,
817
+ pure: target.pure,
818
+ args,
819
+ });
820
+ }
821
+ factKey(context, node) {
822
+ if (node.$type === 'Reference') {
823
+ const binding = context.bindings.get(node.name);
824
+ return binding && isValueType(binding.type) ? node.name : undefined;
825
+ }
826
+ if (node.$type === 'MemberAccess') {
827
+ const root = this.factKey(context, node.object);
828
+ return root ? `${root}.${node.member}` : undefined;
829
+ }
830
+ return undefined;
831
+ }
832
+ facts(node, truth, context) {
833
+ const facts = new Map(context.facts);
834
+ if (node.$type === 'Unary' && node.op === '!')
835
+ return this.facts(node.value, !truth, context);
836
+ if (node.$type === 'Binary') {
837
+ if ((node.op === '==' || node.op === '!=') && (isNone(node.left) || isNone(node.right))) {
838
+ const ref = isNone(node.left) ? node.right : node.left;
839
+ const key = this.factKey(context, ref);
840
+ if (key)
841
+ facts.set(key, truth === (node.op === '!=') ? 'nonNull' : 'none');
842
+ }
843
+ if (node.op === '&&' || node.op === '||') {
844
+ const first = this.facts(node.left, node.op === '&&', context);
845
+ const next = { ...context, facts: first };
846
+ if (truth === (node.op === '&&'))
847
+ return this.facts(node.right, truth, next);
848
+ return intersection([
849
+ this.facts(node.left, truth, context),
850
+ this.facts(node.right, truth, next),
851
+ ]);
852
+ }
853
+ }
854
+ return facts;
855
+ }
856
+ initializer(context, node, expected) {
857
+ if (node.$type !== 'AwaitOperation')
858
+ return this.expression(context, node, expected);
859
+ const result = this.await(context, node);
860
+ if (!result)
861
+ this.fail(node, 'ticks has no value and cannot initialize a binding.');
862
+ return this.expect(context, node, result, expected);
863
+ }
864
+ await(context, node) {
865
+ if (context.mode !== 'process')
866
+ this.fail(node, 'Only processes may await.', 'IR_EFFECT');
867
+ if (node.ticks) {
868
+ const duration = this.expression(context, node.duration, numberType);
869
+ this.emit(context, node, { kind: 'wait', duration: duration.id });
870
+ return undefined;
871
+ }
872
+ const channel = this.symbols.get(node.channel);
873
+ if (channel?.$type !== 'ChannelDecl' || channel.direction !== 'input')
874
+ this.fail(node, 'receive requires an input channel.', 'IR_EFFECT');
875
+ return this.value(context, node, this.type(channel.type), {
876
+ kind: 'receive',
877
+ channel: channel.name,
878
+ });
879
+ }
880
+ markValue(context, name, node, value) {
881
+ for (const key of context.facts.keys())
882
+ if (key === name || key.startsWith(`${name}.`))
883
+ context.facts.delete(key);
884
+ if (value.type.kind !== 'optional')
885
+ return;
886
+ if (node.$type !== 'AwaitOperation' && isNone(node))
887
+ context.facts.set(name, 'none');
888
+ else if (value.nonNull)
889
+ context.facts.set(name, 'nonNull');
890
+ }
891
+ block(context, block) {
892
+ for (const statement of block.statements) {
893
+ if (context.ended)
894
+ this.fail(statement, 'Unreachable statement.', 'IR_CONTROL_FLOW');
895
+ this.statement(context, statement);
896
+ }
897
+ return { operations: context.operations, source: this.source(block) };
898
+ }
899
+ statement(context, node) {
900
+ switch (node.$type) {
901
+ case 'LocalStmt': {
902
+ const type = node.type ? this.type(node.type) : undefined;
903
+ const initial = this.initializer(context, node.value, type);
904
+ const local = context.counter.local++;
905
+ this.bind(context, node.name, {
906
+ kind: 'local',
907
+ index: local,
908
+ type: type ?? initial.type,
909
+ mutable: node.binding === 'let',
910
+ }, node);
911
+ this.emit(context, node, {
912
+ kind: 'local',
913
+ local,
914
+ type: type ?? initial.type,
915
+ initial: initial.id,
916
+ });
917
+ this.markValue(context, node.name, node.value, initial);
918
+ break;
919
+ }
920
+ case 'AssignStmt': {
921
+ const binding = context.bindings.get(node.name);
922
+ if (binding) {
923
+ if (binding.kind !== 'local' || !binding.mutable)
924
+ this.fail(node, `Binding '${node.name}' is readonly.`, 'IR_SCOPE');
925
+ if (!isValueType(binding.type))
926
+ this.fail(node, 'Resources cannot be assigned.');
927
+ const value = this.initializer(context, node.value, binding.type);
928
+ this.emit(context, node, { kind: 'localWrite', local: binding.index, value: value.id });
929
+ this.markValue(context, node.name, node.value, value);
930
+ }
931
+ else {
932
+ if (this.symbols.get(node.name)?.$type !== 'StateDecl')
933
+ this.fail(node, `Name '${node.name}' is not writable.`, 'IR_SCOPE');
934
+ const value = this.initializer(context, node.value, this.symbolType(node.name, node));
935
+ this.emit(context, node, {
936
+ kind: 'write',
937
+ resource: 'state',
938
+ name: node.name,
939
+ value: value.id,
940
+ });
941
+ }
942
+ break;
943
+ }
944
+ case 'IntentStmt': {
945
+ const ref = {
946
+ $type: 'Reference',
947
+ name: node.name,
948
+ $cstNode: node.$cstNode,
949
+ };
950
+ const pin = this.resource(context, ref, 'pin');
951
+ const value = this.expression(context, node.value, intentType);
952
+ this.emit(context, node, { kind: 'writeIntent', pin, value: value.id });
953
+ break;
954
+ }
955
+ case 'CallStmt': {
956
+ if (node.value.$type !== 'Call')
957
+ this.fail(node, 'Only synchronous calls can be used as expression statements.');
958
+ this.call(context, node.value, true);
959
+ break;
960
+ }
961
+ case 'AssertStmt': {
962
+ const condition = this.expression(context, node.condition, boolType);
963
+ let message;
964
+ if (node.message) {
965
+ const child = this.child(context);
966
+ const value = this.expression(child, node.message, stringType);
967
+ this.emit(child, node.message, { kind: 'yield', value: value.id });
968
+ message = {
969
+ type: stringType,
970
+ body: { operations: child.operations, source: this.source(node.message) },
971
+ };
972
+ }
973
+ this.emit(context, node, {
974
+ kind: 'assert',
975
+ condition: condition.id,
976
+ ...(message ? { message } : {}),
977
+ });
978
+ break;
979
+ }
980
+ case 'AwaitStmt':
981
+ this.await(context, node.value);
982
+ break;
983
+ case 'IfStmt':
984
+ this.ifStatement(context, node);
985
+ break;
986
+ case 'MatchStmt':
987
+ this.match(context, node);
988
+ break;
989
+ case 'WhileStmt':
990
+ case 'LoopStmt':
991
+ this.loop(context, node);
992
+ break;
993
+ case 'BreakStmt':
994
+ case 'ContinueStmt': {
995
+ const loop = context.loops.at(-1);
996
+ if (!loop)
997
+ this.fail(node, `${node.$type === 'BreakStmt' ? 'break' : 'continue'} is only valid inside a loop.`, 'IR_CONTROL_FLOW');
998
+ (node.$type === 'BreakStmt' ? loop.breaks : loop.continues).push(new Map(context.facts));
999
+ this.emit(context, node, { kind: node.$type === 'BreakStmt' ? 'break' : 'continue' });
1000
+ context.ended = true;
1001
+ break;
1002
+ }
1003
+ case 'ReturnStmt': {
1004
+ if (context.returns) {
1005
+ if (!node.value)
1006
+ this.fail(node, 'A value-returning function must return a value.', 'IR_CONTROL_FLOW');
1007
+ const value = this.expression(context, node.value, context.returns);
1008
+ this.emit(context, node, { kind: 'returnValue', value: value.id });
1009
+ }
1010
+ else {
1011
+ if (node.value)
1012
+ this.fail(node, 'This body has no return value.', 'IR_CONTROL_FLOW');
1013
+ this.emit(context, node, { kind: 'return' });
1014
+ }
1015
+ context.ended = true;
1016
+ break;
1017
+ }
1018
+ case 'RedirectStmt': {
1019
+ const target = this.expression(context, node.target, numberType);
1020
+ const delay = node.delay
1021
+ ? this.expression(context, node.delay, numberType)
1022
+ : this.constant(context, node, numberType, { kind: 'number', value: 0 });
1023
+ this.emit(context, node, { kind: 'redirect', target: target.id, delay: delay.id });
1024
+ context.ended = true;
1025
+ break;
1026
+ }
1027
+ }
1028
+ }
1029
+ ifStatement(context, node) {
1030
+ const condition = this.expression(context, node.condition, boolType);
1031
+ const yes = this.child(context), no = this.child(context);
1032
+ yes.facts = this.facts(node.condition, true, context);
1033
+ no.facts = this.facts(node.condition, false, context);
1034
+ const yesRegion = this.block(yes, node.yes);
1035
+ let noRegion;
1036
+ if (node.no)
1037
+ noRegion = this.block(no, node.no);
1038
+ else if (node.alternate) {
1039
+ this.ifStatement(no, node.alternate);
1040
+ noRegion = { operations: no.operations, source: this.source(node.alternate) };
1041
+ }
1042
+ this.emit(context, node, {
1043
+ kind: 'if',
1044
+ condition: condition.id,
1045
+ yes: yesRegion,
1046
+ ...(noRegion ? { no: noRegion } : {}),
1047
+ });
1048
+ const paths = [yes, no].filter((branch) => !branch.ended).map((branch) => branch.facts);
1049
+ context.ended = paths.length === 0;
1050
+ context.facts = intersection(paths);
1051
+ }
1052
+ pattern(node) {
1053
+ if (node.number !== undefined)
1054
+ return {
1055
+ kind: 'number',
1056
+ value: (node.sign === '-' ? -1 : 1) * this.number(node, node.number),
1057
+ };
1058
+ if (node.string !== undefined)
1059
+ return { kind: 'string', value: this.string(node, node.string) };
1060
+ if (node.literal === 'none')
1061
+ return { kind: 'none' };
1062
+ if (node.literal)
1063
+ return { kind: 'bool', value: node.literal === 'true' };
1064
+ return { kind: 'enum', name: node.enum, member: node.member };
1065
+ }
1066
+ match(context, node) {
1067
+ const value = this.expression(context, node.value);
1068
+ const arms = [];
1069
+ let otherwise;
1070
+ let excludedNone = false;
1071
+ const paths = [];
1072
+ for (let i = 0; i < node.arms.length; i++) {
1073
+ const arm = node.arms[i];
1074
+ const branch = this.child(context);
1075
+ const name = this.factKey(context, node.value);
1076
+ if (name) {
1077
+ if (arm.pattern.literal === 'none')
1078
+ branch.facts.set(name, 'none');
1079
+ else if (!arm.pattern.wildcard || excludedNone)
1080
+ branch.facts.set(name, 'nonNull');
1081
+ }
1082
+ const body = this.block(branch, arm.body);
1083
+ if (!branch.ended)
1084
+ paths.push(branch.facts);
1085
+ if (arm.pattern.wildcard) {
1086
+ if (i !== node.arms.length - 1)
1087
+ this.fail(arm, 'The default match arm must be last.', 'IR_CONTROL_FLOW');
1088
+ otherwise = body;
1089
+ }
1090
+ else {
1091
+ const pattern = this.pattern(arm.pattern);
1092
+ arms.push({ pattern, body, source: this.source(arm) });
1093
+ if (pattern.kind === 'none')
1094
+ excludedNone = true;
1095
+ }
1096
+ }
1097
+ this.emit(context, node, {
1098
+ kind: 'match',
1099
+ value: value.id,
1100
+ arms,
1101
+ ...(otherwise ? { otherwise } : {}),
1102
+ });
1103
+ context.ended = paths.length === 0;
1104
+ context.facts = intersection(paths);
1105
+ }
1106
+ loop(context, node) {
1107
+ let incoming = new Map(context.facts);
1108
+ // Facts can only disappear at a loop join; recompute until all backedges agree.
1109
+ for (;;) {
1110
+ const entry = this.child(context);
1111
+ entry.facts = incoming;
1112
+ const control = { breaks: [], continues: [] };
1113
+ let test;
1114
+ const body = this.child(entry);
1115
+ body.loops = [...context.loops, control];
1116
+ if (node.$type === 'WhileStmt') {
1117
+ const condition = this.expression(entry, node.condition, boolType);
1118
+ this.emit(entry, node.condition, { kind: 'yield', value: condition.id });
1119
+ test = {
1120
+ type: boolType,
1121
+ body: { operations: entry.operations, source: this.source(node.condition) },
1122
+ };
1123
+ body.facts = this.facts(node.condition, true, entry);
1124
+ }
1125
+ const region = this.block(body, node.body);
1126
+ const backedges = [...control.continues, ...(!body.ended ? [body.facts] : [])];
1127
+ const joined = intersection([incoming, ...backedges]);
1128
+ if (joined.size !== incoming.size) {
1129
+ incoming = joined;
1130
+ continue;
1131
+ }
1132
+ if (test)
1133
+ this.emit(context, node, { kind: 'while', test, body: region });
1134
+ else
1135
+ this.emit(context, node, { kind: 'loop', body: region });
1136
+ const exits = [...control.breaks];
1137
+ if (node.$type === 'WhileStmt')
1138
+ exits.push(this.facts(node.condition, false, entry));
1139
+ context.facts = intersection(exits);
1140
+ context.ended = exits.length === 0;
1141
+ return;
1142
+ }
1143
+ }
1144
+ computation(node, expected) {
1145
+ const context = this.context('computation', expected);
1146
+ const value = this.expression(context, node, expected);
1147
+ this.emit(context, node, { kind: 'returnValue', value: value.id });
1148
+ return {
1149
+ type: expected ?? value.type,
1150
+ body: { operations: context.operations, source: this.source(node) },
1151
+ source: this.source(node),
1152
+ };
1153
+ }
1154
+ options(node, allowed) {
1155
+ const result = new Map();
1156
+ for (const option of node.options) {
1157
+ if (!allowed.includes(option.name) || result.has(option.name))
1158
+ this.fail(option, `Unknown or duplicate option '${option.name}'.`);
1159
+ result.set(option.name, option);
1160
+ }
1161
+ return result;
1162
+ }
1163
+ staticNumber(node, seen = new Set()) {
1164
+ if (node.$type === 'NumberLiteral')
1165
+ return this.number(node, node.value);
1166
+ if (node.$type === 'Literal' && node.value === 'Infinity')
1167
+ return Infinity;
1168
+ if (node.$type === 'Literal' && node.value === 'NaN')
1169
+ return NaN;
1170
+ if (node.$type === 'Unary' && (node.op === '+' || node.op === '-')) {
1171
+ const value = this.staticNumber(node.value, seen);
1172
+ return value === undefined ? undefined : node.op === '-' ? -value : value;
1173
+ }
1174
+ if (node.$type === 'Reference' && !seen.has(node.name)) {
1175
+ const decl = this.symbols.get(node.name);
1176
+ if (decl?.$type === 'ConstantDecl')
1177
+ return this.staticNumber(decl.value, new Set([...seen, node.name]));
1178
+ }
1179
+ return undefined;
1180
+ }
1181
+ sourceType(node) {
1182
+ const [root, ...fields] = node.parts;
1183
+ const decl = this.symbols.get(root);
1184
+ let type;
1185
+ if (decl?.$type === 'PinDecl') {
1186
+ if (fields.shift() !== 'sample')
1187
+ this.fail(node, 'A pin source must begin with pin.sample.');
1188
+ type = { kind: 'sample' };
1189
+ }
1190
+ else if (decl?.$type === 'StateDecl' || decl?.$type === 'DerivedDecl')
1191
+ type = this.symbolType(root, node);
1192
+ else
1193
+ return this.fail(node, 'Expected a state, derived, or pin.sample source.', 'IR_REFERENCE');
1194
+ for (const field of fields)
1195
+ type = this.fieldType(node, type, field);
1196
+ if (base(type).kind === 'intent')
1197
+ this.fail(node, 'Intent values are not comparable observation sources.');
1198
+ return type;
1199
+ }
1200
+ compile() {
1201
+ const declarations = [];
1202
+ for (const property of this.device.properties) {
1203
+ declarations.push({
1204
+ kind: 'property',
1205
+ name: property.name,
1206
+ type: this.type(property.type),
1207
+ ...(property.value
1208
+ ? { default: this.computation(property.value, this.type(property.type)) }
1209
+ : {}),
1210
+ source: this.source(property),
1211
+ });
1212
+ }
1213
+ for (const node of this.device.members) {
1214
+ const source = this.source(node);
1215
+ switch (node.$type) {
1216
+ case 'EnumDecl':
1217
+ declarations.push({ kind: 'enum', name: node.name, members: node.members, source });
1218
+ break;
1219
+ case 'RecordDecl':
1220
+ declarations.push({
1221
+ kind: 'record',
1222
+ name: node.name,
1223
+ fields: node.fields.map((field) => ({ name: field.name, type: field.type })),
1224
+ source,
1225
+ });
1226
+ break;
1227
+ case 'ConstantDecl':
1228
+ declarations.push({
1229
+ kind: 'const',
1230
+ name: node.name,
1231
+ type: this.symbolType(node.name, node),
1232
+ value: this.computation(node.value, this.symbolType(node.name, node)),
1233
+ source,
1234
+ });
1235
+ break;
1236
+ case 'StateDecl':
1237
+ declarations.push({
1238
+ kind: 'state',
1239
+ name: node.name,
1240
+ type: this.type(node.type),
1241
+ initial: this.computation(node.value, this.type(node.type)),
1242
+ source,
1243
+ });
1244
+ break;
1245
+ case 'DerivedDecl':
1246
+ declarations.push({
1247
+ kind: 'derived',
1248
+ name: node.name,
1249
+ type: this.type(node.type),
1250
+ value: this.computation(node.value, this.type(node.type)),
1251
+ source,
1252
+ });
1253
+ break;
1254
+ case 'PinDecl': {
1255
+ const opts = this.options(node, ['low', 'high']);
1256
+ if (opts.has('low') !== opts.has('high'))
1257
+ this.fail(node, 'Pin thresholds require both low and high.');
1258
+ if (opts.has('low')) {
1259
+ const low = this.staticNumber(opts.get('low').value), high = this.staticNumber(opts.get('high').value);
1260
+ if ((low !== undefined && !Number.isFinite(low)) ||
1261
+ (high !== undefined && !Number.isFinite(high)) ||
1262
+ (low !== undefined && high !== undefined && low >= high))
1263
+ this.fail(node, 'Pin thresholds must be finite and satisfy low < high.');
1264
+ }
1265
+ declarations.push({
1266
+ kind: 'pin',
1267
+ name: node.name,
1268
+ ...(opts.has('low')
1269
+ ? {
1270
+ low: this.computation(opts.get('low').value, numberType),
1271
+ high: this.computation(opts.get('high').value, numberType),
1272
+ }
1273
+ : {}),
1274
+ ...(node.value
1275
+ ? {
1276
+ initial: this.computation(node.value, intentType),
1277
+ }
1278
+ : {}),
1279
+ source,
1280
+ });
1281
+ break;
1282
+ }
1283
+ case 'MemoryDecl':
1284
+ declarations.push({
1285
+ kind: 'memory',
1286
+ name: node.name,
1287
+ public: node.public,
1288
+ length: this.computation(node.length, numberType),
1289
+ source,
1290
+ });
1291
+ break;
1292
+ case 'ChannelDecl': {
1293
+ const opts = this.options(node, ['capacity', 'maxLength', 'overflow']);
1294
+ if (!opts.has('capacity'))
1295
+ this.fail(node, 'Channel capacity is required.');
1296
+ const type = this.type(node.type);
1297
+ if ((type.kind === 'bytes') !== opts.has('maxLength'))
1298
+ this.fail(node, 'Exactly bytes channels require maxLength.');
1299
+ declarations.push({
1300
+ kind: 'channel',
1301
+ name: node.name,
1302
+ direction: node.direction,
1303
+ type,
1304
+ capacity: this.computation(opts.get('capacity').value, numberType),
1305
+ ...(opts.has('maxLength')
1306
+ ? { maxLength: this.computation(opts.get('maxLength').value, numberType) }
1307
+ : {}),
1308
+ ...(opts.has('overflow')
1309
+ ? {
1310
+ overflow: this.computation(opts.get('overflow').value, {
1311
+ kind: 'enum',
1312
+ name: 'Overflow',
1313
+ }),
1314
+ }
1315
+ : {}),
1316
+ source,
1317
+ });
1318
+ break;
1319
+ }
1320
+ case 'NotificationDecl':
1321
+ declarations.push({
1322
+ kind: 'notification',
1323
+ name: node.name,
1324
+ direction: node.direction,
1325
+ source,
1326
+ });
1327
+ break;
1328
+ case 'FunctionDecl': {
1329
+ const returns = node.returns ? this.type(node.returns) : undefined;
1330
+ const context = this.context('function', returns);
1331
+ const params = node.params.map((param, index) => {
1332
+ const type = this.parameterType(param.type);
1333
+ this.bind(context, param.name, { kind: 'parameter', index, type, mutable: false }, param);
1334
+ return { name: param.name, type, source: this.source(param) };
1335
+ });
1336
+ const body = this.block(context, node.body);
1337
+ if (returns && !context.ended)
1338
+ this.fail(node, 'A value-returning function may reach its end.', 'IR_CONTROL_FLOW');
1339
+ declarations.push({
1340
+ kind: 'function',
1341
+ name: node.name,
1342
+ pure: node.pure,
1343
+ params,
1344
+ ...(returns ? { returns } : {}),
1345
+ body,
1346
+ source,
1347
+ });
1348
+ break;
1349
+ }
1350
+ case 'ProcessDecl': {
1351
+ const body = this.block(this.context('process'), node.body);
1352
+ if (node.name === 'start' && !node.path)
1353
+ declarations.push({ kind: 'start', body, source });
1354
+ else {
1355
+ if (!node.path || node.name === 'start')
1356
+ this.fail(node, 'A non-start process requires an on source.');
1357
+ const endpoint = this.symbols.get(node.path.parts[0]);
1358
+ if (!(node.path.parts.length === 1 &&
1359
+ endpoint?.$type === 'NotificationDecl' &&
1360
+ endpoint.direction === 'input'))
1361
+ this.sourceType(node.path);
1362
+ declarations.push({
1363
+ kind: 'on',
1364
+ name: node.name,
1365
+ sourcePath: node.path.parts,
1366
+ body,
1367
+ source,
1368
+ });
1369
+ }
1370
+ break;
1371
+ }
1372
+ case 'ReactionDecl': {
1373
+ if (node.name === 'start')
1374
+ this.fail(node, 'start is reserved for the startup process.', 'IR_SCOPE');
1375
+ const sourceType = this.sourceType(node.path);
1376
+ const captures = node.captures.map((capture) => {
1377
+ const value = this.computation(capture.value);
1378
+ if (base(value.type).kind === 'intent')
1379
+ this.fail(capture, 'Reaction captures must be comparable.');
1380
+ return {
1381
+ name: capture.name,
1382
+ value: value,
1383
+ source: this.source(capture),
1384
+ };
1385
+ });
1386
+ const context = this.context('reaction');
1387
+ this.bind(context, node.previous, { kind: 'previous', type: sourceType, index: 0, mutable: false }, node);
1388
+ this.bind(context, node.current, { kind: 'current', type: sourceType, index: 0, mutable: false }, node);
1389
+ captures.forEach((capture, index) => this.bind(context, capture.name, { kind: 'capture', type: capture.value.type, index, mutable: false }, node.captures[index]));
1390
+ declarations.push({
1391
+ kind: 'reaction',
1392
+ name: node.name,
1393
+ sourcePath: node.path.parts,
1394
+ sourceType,
1395
+ previous: node.previous,
1396
+ current: node.current,
1397
+ captures,
1398
+ body: this.block(context, node.body),
1399
+ source,
1400
+ });
1401
+ break;
1402
+ }
1403
+ case 'AssertStmt': {
1404
+ const context = this.context('assert');
1405
+ this.statement(context, node);
1406
+ declarations.push({
1407
+ kind: 'assert',
1408
+ body: { operations: context.operations, source },
1409
+ source,
1410
+ });
1411
+ break;
1412
+ }
1413
+ }
1414
+ }
1415
+ return validateDeviceIR({
1416
+ kind: 'device',
1417
+ name: this.device.name,
1418
+ declarations,
1419
+ source: this.source(this.device),
1420
+ });
1421
+ }
1422
+ }
1423
+ //# sourceMappingURL=compiler.js.map