@schemd/core 0.2.0 → 0.3.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.
- package/CHANGELOG.md +46 -0
- package/README.md +91 -27
- package/dist/compiler.d.ts +39 -0
- package/dist/compiler.js +17 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout.d.ts +49 -2
- package/dist/layout.js +556 -73
- package/dist/math-label.js +1 -22
- package/dist/parser.js +417 -42
- package/dist/renderer.js +377 -65
- package/dist/route-cache.d.ts +6 -0
- package/dist/route-cache.js +10 -0
- package/dist/types.d.ts +124 -19
- package/dist/types.js +167 -4
- package/dist/xml.d.ts +6 -0
- package/dist/xml.js +38 -0
- package/package.json +83 -74
package/dist/parser.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ANALOG_KINDS, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SchematicSyntaxError } from './types.js';
|
|
1
|
+
import { ADDER_TYPES, AMPLIFIER_TYPES, ANALOG_KINDS, BUFFER_TYPES, BUS_TYPES, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIGITAL_COMPONENT_KINDS, DIODE_TYPES, ELECTRICAL_COMPONENT_KINDS, FLIPFLOP_TYPES, GROUND_STYLES, LOAD_TYPES, LOGIC_STATES, METER_TYPES, MUX_TYPES, NAMED_QUANTUM_GATE_KINDS, PASSIVE_KINDS, POWER_TYPES, PROTECTION_TYPES, QUANTUM_GATE_KINDS, QUANTUM_SPECIAL_KINDS, RESONATOR_TYPES, SCHEMATIC_ORIENTATIONS, SCHEMATIC_SIGNAL_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, SOURCE_TYPES, SWITCH_TYPES, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
2
|
import { validateDocumentGeometry } from './layout.js';
|
|
3
3
|
import { mathLabelTextWidth } from './math-label.js';
|
|
4
|
+
import { cacheParsedSchematicRoutes } from './route-cache.js';
|
|
4
5
|
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
5
6
|
const COMPONENT_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*):([A-Za-z][A-Za-z0-9_-]*)\s+"([^"]+)"\s+at\s+\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)\s+(.+)$/;
|
|
6
|
-
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_
|
|
7
|
+
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_+-]*)\s*->\s*([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_+-]*)\s+(.+)$/;
|
|
7
8
|
const SCHEMD_FENCE_PATTERN = /^schemd\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
|
|
8
9
|
const NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
9
10
|
const ANGLE_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:deg|grad|rad|turn)?$/i;
|
|
@@ -31,7 +32,11 @@ function freezeParsedDocument(document) {
|
|
|
31
32
|
Object.freeze(component.pins.bottom);
|
|
32
33
|
Object.freeze(component.pins);
|
|
33
34
|
}
|
|
34
|
-
if (component.kind === 'class'
|
|
35
|
+
if (component.kind === 'class' ||
|
|
36
|
+
component.kind === 'interface' ||
|
|
37
|
+
component.kind === 'enumeration' ||
|
|
38
|
+
component.kind === 'datatype' ||
|
|
39
|
+
component.kind === 'object') {
|
|
35
40
|
Object.freeze(component.attributes);
|
|
36
41
|
Object.freeze(component.operations);
|
|
37
42
|
}
|
|
@@ -274,10 +279,26 @@ function parseEnumOption(value, fallback, values, name, line) {
|
|
|
274
279
|
if (value === undefined)
|
|
275
280
|
return fallback;
|
|
276
281
|
if (!includesValue(values, value)) {
|
|
277
|
-
throw new SchematicSyntaxError(
|
|
282
|
+
throw new SchematicSyntaxError(`Option ${name} must be one of: ${values.join(', ')}.`, line);
|
|
278
283
|
}
|
|
279
284
|
return value;
|
|
280
285
|
}
|
|
286
|
+
function parseOrientation(attributes, line) {
|
|
287
|
+
const value = attributes.get('orientation');
|
|
288
|
+
return value === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
orientation: parseEnumOption(value, 'right', SCHEMATIC_ORIENTATIONS, 'orientation', line)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function parseWidth(value, fallback, line) {
|
|
295
|
+
if (value === undefined)
|
|
296
|
+
return fallback;
|
|
297
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 256) {
|
|
298
|
+
throw new SchematicSyntaxError('width must be an integer from 1 through 256.', line);
|
|
299
|
+
}
|
|
300
|
+
return Number(value);
|
|
301
|
+
}
|
|
281
302
|
function parseIcPins(attributes, line) {
|
|
282
303
|
const registered = new Set();
|
|
283
304
|
const parseSide = (side) => {
|
|
@@ -371,39 +392,81 @@ function parseComponent(match, line) {
|
|
|
371
392
|
const attributes = parseAttributes(tail.options, line);
|
|
372
393
|
const common = commonComponent(match, tail.color === '' ? 'slate' : tail.color, line);
|
|
373
394
|
if (includesValue(PASSIVE_KINDS, kind)) {
|
|
374
|
-
assertOnlyAttributes(attributes, [], line);
|
|
375
|
-
|
|
395
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
396
|
+
const allowed = kind === 'resistor'
|
|
397
|
+
? ['fixed', 'variable', 'rheostat', 'potentiometer', 'thermistor', 'ldr']
|
|
398
|
+
: kind === 'capacitor'
|
|
399
|
+
? ['fixed', 'variable', 'polarized']
|
|
400
|
+
: ['fixed', 'coupled', 'transformer'];
|
|
401
|
+
const passiveType = parseEnumOption(attributes.get('type'), 'fixed', allowed, 'type', line);
|
|
402
|
+
return passiveType === 'fixed'
|
|
403
|
+
? { kind, ...common, ...parseOrientation(attributes, line) }
|
|
404
|
+
: { kind, ...common, passiveType, ...parseOrientation(attributes, line) };
|
|
376
405
|
}
|
|
377
406
|
if (includesValue(ANALOG_KINDS, kind)) {
|
|
378
407
|
switch (kind) {
|
|
379
408
|
case 'diode':
|
|
380
|
-
assertOnlyAttributes(attributes, ['type'], line);
|
|
409
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
381
410
|
return {
|
|
382
411
|
kind,
|
|
383
412
|
...common,
|
|
384
|
-
diodeType: parseEnumOption(attributes.get('type'), 'standard', DIODE_TYPES, 'type', line)
|
|
413
|
+
diodeType: parseEnumOption(attributes.get('type'), 'standard', DIODE_TYPES, 'type', line),
|
|
414
|
+
...parseOrientation(attributes, line)
|
|
385
415
|
};
|
|
386
416
|
case 'transistor':
|
|
387
|
-
assertOnlyAttributes(attributes, ['type'], line);
|
|
417
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
388
418
|
return {
|
|
389
419
|
kind,
|
|
390
420
|
...common,
|
|
391
|
-
transistorType: parseEnumOption(attributes.get('type'), 'npn', TRANSISTOR_TYPES, 'type', line)
|
|
421
|
+
transistorType: parseEnumOption(attributes.get('type'), 'npn', TRANSISTOR_TYPES, 'type', line),
|
|
422
|
+
...parseOrientation(attributes, line)
|
|
392
423
|
};
|
|
393
|
-
case 'port':
|
|
394
|
-
assertOnlyAttributes(attributes, [], line);
|
|
395
|
-
|
|
424
|
+
case 'port': {
|
|
425
|
+
assertOnlyAttributes(attributes, ['width', 'orientation'], line);
|
|
426
|
+
const width = parseWidth(attributes.get('width'), 1, line);
|
|
427
|
+
return width === 1
|
|
428
|
+
? { kind, ...common, ...parseOrientation(attributes, line) }
|
|
429
|
+
: { kind, ...common, width, ...parseOrientation(attributes, line) };
|
|
430
|
+
}
|
|
396
431
|
case 'ground':
|
|
397
|
-
assertOnlyAttributes(attributes, ['style'], line);
|
|
432
|
+
assertOnlyAttributes(attributes, ['style', 'orientation'], line);
|
|
398
433
|
return {
|
|
399
434
|
kind,
|
|
400
435
|
...common,
|
|
401
|
-
groundStyle: parseEnumOption(attributes.get('style'), 'signal', GROUND_STYLES, 'style', line)
|
|
436
|
+
groundStyle: parseEnumOption(attributes.get('style'), 'signal', GROUND_STYLES, 'style', line),
|
|
437
|
+
...parseOrientation(attributes, line)
|
|
402
438
|
};
|
|
403
439
|
}
|
|
404
440
|
}
|
|
441
|
+
if (includesValue(ELECTRICAL_COMPONENT_KINDS, kind)) {
|
|
442
|
+
if (kind === 'junction' || kind === 'testpoint') {
|
|
443
|
+
assertOnlyAttributes(attributes, [], line);
|
|
444
|
+
return { kind, ...common };
|
|
445
|
+
}
|
|
446
|
+
if (kind === 'connector') {
|
|
447
|
+
assertOnlyAttributes(attributes, ['orientation'], line);
|
|
448
|
+
return { kind, ...common, ...parseOrientation(attributes, line) };
|
|
449
|
+
}
|
|
450
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
451
|
+
const variant = kind === 'source'
|
|
452
|
+
? parseEnumOption(attributes.get('type'), 'voltage-dc', SOURCE_TYPES, 'type', line)
|
|
453
|
+
: kind === 'power'
|
|
454
|
+
? parseEnumOption(attributes.get('type'), 'vcc', POWER_TYPES, 'type', line)
|
|
455
|
+
: kind === 'switch'
|
|
456
|
+
? parseEnumOption(attributes.get('type'), 'spst', SWITCH_TYPES, 'type', line)
|
|
457
|
+
: kind === 'protection'
|
|
458
|
+
? parseEnumOption(attributes.get('type'), 'fuse', PROTECTION_TYPES, 'type', line)
|
|
459
|
+
: kind === 'amplifier'
|
|
460
|
+
? parseEnumOption(attributes.get('type'), 'opamp', AMPLIFIER_TYPES, 'type', line)
|
|
461
|
+
: kind === 'resonator'
|
|
462
|
+
? parseEnumOption(attributes.get('type'), 'crystal', RESONATOR_TYPES, 'type', line)
|
|
463
|
+
: kind === 'meter'
|
|
464
|
+
? parseEnumOption(attributes.get('type'), 'voltmeter', METER_TYPES, 'type', line)
|
|
465
|
+
: parseEnumOption(attributes.get('type'), 'lamp', LOAD_TYPES, 'type', line);
|
|
466
|
+
return { kind, ...common, variant, ...parseOrientation(attributes, line) };
|
|
467
|
+
}
|
|
405
468
|
if (includesValue(CLASSICAL_GATE_KINDS, kind)) {
|
|
406
|
-
assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard'], line);
|
|
469
|
+
assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard', 'orientation'], line);
|
|
407
470
|
const standard = attributes.get('standard') ?? 'ieee';
|
|
408
471
|
if (standard !== 'ieee' && standard !== 'iec') {
|
|
409
472
|
throw new SchematicSyntaxError('standard must be ieee or iec.', line);
|
|
@@ -413,26 +476,93 @@ function parseComponent(match, line) {
|
|
|
413
476
|
...common,
|
|
414
477
|
inputs: parseCount(attributes.get('inputs'), kind === 'not' ? 1 : 2, 'inputs', line),
|
|
415
478
|
outputs: parseCount(attributes.get('outputs'), 1, 'outputs', line),
|
|
416
|
-
standard
|
|
479
|
+
standard,
|
|
480
|
+
...parseOrientation(attributes, line)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (includesValue(DIGITAL_COMPONENT_KINDS, kind)) {
|
|
484
|
+
assertOnlyAttributes(attributes, ['type', 'inputs', 'outputs', 'width', 'orientation'], line);
|
|
485
|
+
const defaults = {
|
|
486
|
+
buffer: [1, 1], logic: [0, 1], clock: [0, 1], flipflop: [3, 2], mux: [2, 1],
|
|
487
|
+
encoder: [4, 2], decoder: [2, 4], register: [3, 2], counter: [3, 2], adder: [2, 2],
|
|
488
|
+
comparator: [2, 3], bus: [1, 2]
|
|
489
|
+
};
|
|
490
|
+
const [defaultInputs, defaultOutputs] = defaults[kind];
|
|
491
|
+
let inputs = parseCount(attributes.get('inputs'), defaultInputs === 0 ? 1 : defaultInputs, 'inputs', line);
|
|
492
|
+
let outputs = parseCount(attributes.get('outputs'), defaultOutputs, 'outputs', line);
|
|
493
|
+
if ((kind === 'logic' || kind === 'clock') && attributes.has('inputs')) {
|
|
494
|
+
throw new SchematicSyntaxError(`${kind} does not accept inputs.`, line);
|
|
495
|
+
}
|
|
496
|
+
const width = parseWidth(attributes.get('width'), kind === 'bus' || kind === 'register' ? 8 : 1, line);
|
|
497
|
+
if (attributes.has('width') && kind !== 'bus' && kind !== 'register') {
|
|
498
|
+
throw new SchematicSyntaxError(`Option width is not supported for ${kind}.`, line);
|
|
499
|
+
}
|
|
500
|
+
if (kind === 'bus' && width < 2) {
|
|
501
|
+
throw new SchematicSyntaxError('Bus width must be at least 2.', line);
|
|
502
|
+
}
|
|
503
|
+
const variant = kind === 'buffer'
|
|
504
|
+
? parseEnumOption(attributes.get('type'), 'plain', BUFFER_TYPES, 'type', line)
|
|
505
|
+
: kind === 'logic'
|
|
506
|
+
? parseEnumOption(attributes.get('type'), 'low', LOGIC_STATES, 'type', line)
|
|
507
|
+
: kind === 'flipflop'
|
|
508
|
+
? parseEnumOption(attributes.get('type'), 'd', FLIPFLOP_TYPES, 'type', line)
|
|
509
|
+
: kind === 'mux'
|
|
510
|
+
? parseEnumOption(attributes.get('type'), 'mux', MUX_TYPES, 'type', line)
|
|
511
|
+
: kind === 'adder'
|
|
512
|
+
? parseEnumOption(attributes.get('type'), 'half', ADDER_TYPES, 'type', line)
|
|
513
|
+
: kind === 'bus'
|
|
514
|
+
? parseEnumOption(attributes.get('type'), 'splitter', BUS_TYPES, 'type', line)
|
|
515
|
+
: undefined;
|
|
516
|
+
if (variant === undefined && attributes.has('type')) {
|
|
517
|
+
throw new SchematicSyntaxError(`Option type is not supported for ${kind}.`, line);
|
|
518
|
+
}
|
|
519
|
+
if (kind === 'adder' && variant === 'full' && !attributes.has('inputs'))
|
|
520
|
+
inputs = 3;
|
|
521
|
+
if (kind === 'mux' && variant === 'demux') {
|
|
522
|
+
if (!attributes.has('inputs'))
|
|
523
|
+
inputs = 1;
|
|
524
|
+
if (!attributes.has('outputs'))
|
|
525
|
+
outputs = 2;
|
|
526
|
+
}
|
|
527
|
+
if (kind === 'bus' && variant === 'joiner' && !attributes.has('outputs'))
|
|
528
|
+
outputs = 1;
|
|
529
|
+
if ((kind === 'buffer' && (inputs !== 1 || outputs !== 1)) || ((kind === 'logic' || kind === 'clock') && outputs !== 1)) {
|
|
530
|
+
throw new SchematicSyntaxError(`${kind} has fixed terminal counts.`, line);
|
|
531
|
+
}
|
|
532
|
+
const bodyHeight = Math.max(52, Math.max(inputs, outputs) * 16 + 20);
|
|
533
|
+
const digital = {
|
|
534
|
+
kind, ...common, inputs: kind === 'logic' || kind === 'clock' ? 0 : inputs, outputs, width,
|
|
535
|
+
bodyWidth: kind === 'buffer' || kind === 'logic' || kind === 'clock' ? 56 : 84,
|
|
536
|
+
bodyHeight,
|
|
537
|
+
...parseOrientation(attributes, line)
|
|
417
538
|
};
|
|
539
|
+
if (variant !== undefined)
|
|
540
|
+
digital.variant = variant;
|
|
541
|
+
return digital;
|
|
418
542
|
}
|
|
419
543
|
if (kind === 'ic') {
|
|
420
|
-
assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom'], line);
|
|
544
|
+
assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom', 'orientation'], line);
|
|
421
545
|
const pins = parseIcPins(attributes, line);
|
|
422
546
|
return {
|
|
423
547
|
kind,
|
|
424
548
|
...common,
|
|
425
549
|
pins,
|
|
426
|
-
...integratedCircuitDimensions(pins)
|
|
550
|
+
...integratedCircuitDimensions(pins),
|
|
551
|
+
...parseOrientation(attributes, line)
|
|
427
552
|
};
|
|
428
553
|
}
|
|
429
554
|
if (includesValue(UML_COMPONENT_KINDS, kind)) {
|
|
430
555
|
switch (kind) {
|
|
431
|
-
case 'class':
|
|
556
|
+
case 'class':
|
|
557
|
+
case 'interface':
|
|
558
|
+
case 'enumeration':
|
|
559
|
+
case 'datatype':
|
|
560
|
+
case 'object': {
|
|
432
561
|
assertOnlyAttributes(attributes, ['attributes', 'operations', 'stereotype', 'width'], line);
|
|
433
562
|
const classAttributes = parseUmlRows(attributes.get('attributes'), 'attributes', line);
|
|
434
563
|
const operations = parseUmlRows(attributes.get('operations'), 'operations', line);
|
|
435
|
-
const stereotype = attributes.get('stereotype')
|
|
564
|
+
const stereotype = attributes.get('stereotype') ??
|
|
565
|
+
(kind === 'interface' || kind === 'enumeration' || kind === 'datatype' ? kind : undefined);
|
|
436
566
|
if (stereotype !== undefined && (stereotype === '' || stereotype.length > 128)) {
|
|
437
567
|
throw new SchematicSyntaxError('stereotype must contain 1 through 128 characters.', line);
|
|
438
568
|
}
|
|
@@ -466,13 +596,33 @@ function parseComponent(match, line) {
|
|
|
466
596
|
bodyHeight: 40 + details.length * UML_ROW_HEIGHT
|
|
467
597
|
};
|
|
468
598
|
}
|
|
599
|
+
case 'history':
|
|
600
|
+
assertOnlyAttributes(attributes, ['type'], line);
|
|
601
|
+
return {
|
|
602
|
+
kind,
|
|
603
|
+
...common,
|
|
604
|
+
variant: parseEnumOption(attributes.get('type'), 'shallow', ['shallow', 'deep'], 'type', line)
|
|
605
|
+
};
|
|
469
606
|
case 'usecase':
|
|
470
607
|
case 'lifeline':
|
|
471
608
|
case 'note':
|
|
472
|
-
case 'package':
|
|
609
|
+
case 'package':
|
|
610
|
+
case 'component':
|
|
611
|
+
case 'artifact':
|
|
612
|
+
case 'node':
|
|
613
|
+
case 'device':
|
|
614
|
+
case 'execution':
|
|
615
|
+
case 'system':
|
|
616
|
+
case 'action':
|
|
617
|
+
case 'object-node':
|
|
618
|
+
case 'partition':
|
|
619
|
+
case 'activation':
|
|
620
|
+
case 'fragment':
|
|
621
|
+
case 'interaction':
|
|
622
|
+
case 'region': {
|
|
473
623
|
assertOnlyAttributes(attributes, ['width', 'height'], line);
|
|
474
624
|
const defaultWidth = Math.max(kind === 'usecase' ? 112 : 96, mathLabelTextWidth(common.label, 8) + 28);
|
|
475
|
-
const defaultHeight = kind === 'lifeline' ? 180 : kind === 'usecase' ? 56 : 64;
|
|
625
|
+
const defaultHeight = kind === 'lifeline' ? 180 : kind === 'activation' ? 96 : kind === 'usecase' ? 56 : 64;
|
|
476
626
|
return {
|
|
477
627
|
kind,
|
|
478
628
|
...common,
|
|
@@ -481,25 +631,87 @@ function parseComponent(match, line) {
|
|
|
481
631
|
};
|
|
482
632
|
}
|
|
483
633
|
case 'actor':
|
|
634
|
+
case 'provided-interface':
|
|
635
|
+
case 'required-interface':
|
|
636
|
+
case 'component-port':
|
|
637
|
+
case 'decision':
|
|
638
|
+
case 'merge':
|
|
639
|
+
case 'fork':
|
|
640
|
+
case 'join':
|
|
641
|
+
case 'activity-final':
|
|
642
|
+
case 'flow-final':
|
|
643
|
+
case 'send-signal':
|
|
644
|
+
case 'receive-signal':
|
|
645
|
+
case 'destruction':
|
|
646
|
+
case 'gate':
|
|
647
|
+
case 'found':
|
|
648
|
+
case 'lost':
|
|
649
|
+
case 'choice':
|
|
650
|
+
case 'state-junction':
|
|
651
|
+
case 'entry':
|
|
652
|
+
case 'exit':
|
|
653
|
+
case 'terminate':
|
|
484
654
|
case 'initial':
|
|
485
655
|
case 'final':
|
|
486
656
|
assertOnlyAttributes(attributes, [], line);
|
|
487
657
|
return { kind, ...common };
|
|
488
658
|
}
|
|
489
659
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
660
|
+
if (includesValue(QUANTUM_GATE_KINDS, kind)) {
|
|
661
|
+
const detailed = kind === 'qgate' || includesValue(NAMED_QUANTUM_GATE_KINDS, kind);
|
|
662
|
+
assertOnlyAttributes(attributes, detailed ? ['parameter', 'matrix', 'phase', 'orientation'] : ['orientation'], line);
|
|
663
|
+
const component = { kind, ...common, ...parseOrientation(attributes, line) };
|
|
664
|
+
if (detailed) {
|
|
665
|
+
const parameter = attributes.get('parameter');
|
|
666
|
+
const matrix = attributes.get('matrix');
|
|
667
|
+
const phase = attributes.get('phase');
|
|
668
|
+
if (parameter !== undefined)
|
|
669
|
+
component.parameter = parameter;
|
|
670
|
+
if (matrix !== undefined)
|
|
671
|
+
component.matrix = matrix;
|
|
672
|
+
if (phase !== undefined)
|
|
673
|
+
component.phase = phase;
|
|
674
|
+
}
|
|
675
|
+
return component;
|
|
676
|
+
}
|
|
677
|
+
if (includesValue(QUANTUM_SPECIAL_KINDS, kind)) {
|
|
678
|
+
assertOnlyAttributes(attributes, ['control', 'operator', 'controls', 'targets', 'wires', 'width', 'orientation'], line);
|
|
679
|
+
if (attributes.has('control') && kind !== 'control' && kind !== 'controlled') {
|
|
680
|
+
throw new SchematicSyntaxError(`Option control is not supported for ${kind}.`, line);
|
|
681
|
+
}
|
|
682
|
+
if (attributes.has('operator') && kind !== 'controlled') {
|
|
683
|
+
throw new SchematicSyntaxError(`Option operator is not supported for ${kind}.`, line);
|
|
684
|
+
}
|
|
685
|
+
if ((attributes.has('controls') || attributes.has('targets')) && !['controlled', 'cz', 'cphase', 'toffoli', 'swap'].includes(kind)) {
|
|
686
|
+
throw new SchematicSyntaxError(`Control/target counts are not supported for ${kind}.`, line);
|
|
687
|
+
}
|
|
688
|
+
if (attributes.has('wires') && kind !== 'barrier' && kind !== 'delay') {
|
|
689
|
+
throw new SchematicSyntaxError(`Option wires is not supported for ${kind}.`, line);
|
|
690
|
+
}
|
|
691
|
+
if (attributes.has('width') && kind !== 'classical-register') {
|
|
692
|
+
throw new SchematicSyntaxError(`Option width is not supported for ${kind}.`, line);
|
|
693
|
+
}
|
|
694
|
+
const controlType = parseEnumOption(attributes.get('control'), 'positive', ['positive', 'negative', 'classical'], 'control', line);
|
|
695
|
+
const controlled = kind === 'controlled' || kind === 'cz' || kind === 'cphase' || kind === 'toffoli';
|
|
696
|
+
const component = {
|
|
697
|
+
kind, ...common,
|
|
698
|
+
controls: parseCount(attributes.get('controls'), kind === 'toffoli' ? 2 : controlled ? 1 : 0, 'controls', line),
|
|
699
|
+
targets: parseCount(attributes.get('targets'), kind === 'swap' ? 2 : controlled ? 1 : 0, 'targets', line),
|
|
700
|
+
wires: parseCount(attributes.get('wires'), kind === 'barrier' || kind === 'delay' ? 2 : 1, 'wires', line),
|
|
701
|
+
width: parseWidth(attributes.get('width'), kind === 'classical-register' ? 8 : 1, line),
|
|
702
|
+
...parseOrientation(attributes, line)
|
|
703
|
+
};
|
|
704
|
+
if (kind === 'classical-register' && component.width < 2) {
|
|
705
|
+
throw new SchematicSyntaxError('Classical register width must be at least 2.', line);
|
|
706
|
+
}
|
|
707
|
+
if (kind === 'control' || kind === 'controlled')
|
|
708
|
+
component.controlType = controlType;
|
|
709
|
+
const operator = attributes.get('operator');
|
|
710
|
+
if (operator !== undefined)
|
|
711
|
+
component.operator = operator;
|
|
712
|
+
return component;
|
|
713
|
+
}
|
|
714
|
+
throw new SchematicSyntaxError(`Unsupported component kind ${kind}.`, line);
|
|
503
715
|
}
|
|
504
716
|
function parseEndpoint(componentId, port) {
|
|
505
717
|
return { componentId, port };
|
|
@@ -537,8 +749,10 @@ function parseConnectionOptions(raw, line) {
|
|
|
537
749
|
let relation = 'signal';
|
|
538
750
|
let label;
|
|
539
751
|
let dashed = false;
|
|
752
|
+
let signalKind;
|
|
753
|
+
let width;
|
|
540
754
|
if (raw === undefined || raw.trim() === '') {
|
|
541
|
-
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
755
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
542
756
|
}
|
|
543
757
|
const seen = new Set();
|
|
544
758
|
for (const token of connectionOptionTokens(raw.trim(), line)) {
|
|
@@ -566,6 +780,14 @@ function parseConnectionOptions(raw, line) {
|
|
|
566
780
|
seen.add('relation');
|
|
567
781
|
continue;
|
|
568
782
|
}
|
|
783
|
+
if (includesValue(SCHEMATIC_SIGNAL_KINDS, token)) {
|
|
784
|
+
if (seen.has('signal')) {
|
|
785
|
+
throw new SchematicSyntaxError('Connection signal kind can only be declared once.', line);
|
|
786
|
+
}
|
|
787
|
+
signalKind = token;
|
|
788
|
+
seen.add('signal');
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
569
791
|
if (token === 'arrow' || token === 'dot') {
|
|
570
792
|
if (seen.has('marker-end')) {
|
|
571
793
|
throw new SchematicSyntaxError('Connection marker-end can only be declared once.', line);
|
|
@@ -574,7 +796,7 @@ function parseConnectionOptions(raw, line) {
|
|
|
574
796
|
seen.add('marker-end');
|
|
575
797
|
continue;
|
|
576
798
|
}
|
|
577
|
-
const match = token.match(/^(marker-start|marker-end|relation|label)=(.*)$/);
|
|
799
|
+
const match = token.match(/^(marker-start|marker-end|relation|label|signal|width)=(.*)$/);
|
|
578
800
|
if (!match) {
|
|
579
801
|
throw new SchematicSyntaxError('Unsupported connection routing, marker, relation, label, or stroke option.', line);
|
|
580
802
|
}
|
|
@@ -601,6 +823,15 @@ function parseConnectionOptions(raw, line) {
|
|
|
601
823
|
}
|
|
602
824
|
label = optionValue;
|
|
603
825
|
}
|
|
826
|
+
else if (option === 'signal') {
|
|
827
|
+
if (!includesValue(SCHEMATIC_SIGNAL_KINDS, optionValue)) {
|
|
828
|
+
throw new SchematicSyntaxError(`signal must be one of: ${SCHEMATIC_SIGNAL_KINDS.join(', ')}.`, line);
|
|
829
|
+
}
|
|
830
|
+
signalKind = optionValue;
|
|
831
|
+
}
|
|
832
|
+
else if (option === 'width') {
|
|
833
|
+
width = parseWidth(optionValue, 1, line);
|
|
834
|
+
}
|
|
604
835
|
else {
|
|
605
836
|
const marker = parseSignalMarker(optionValue, option, line);
|
|
606
837
|
if (option === 'marker-start')
|
|
@@ -621,19 +852,25 @@ function parseConnectionOptions(raw, line) {
|
|
|
621
852
|
markerEnd = 'triangle';
|
|
622
853
|
else if (relation === 'dependency' ||
|
|
623
854
|
relation === 'message' ||
|
|
855
|
+
relation === 'asynchronous' ||
|
|
856
|
+
relation === 'return' ||
|
|
857
|
+
relation === 'control-flow' ||
|
|
858
|
+
relation === 'object-flow' ||
|
|
624
859
|
relation === 'transition' ||
|
|
625
860
|
relation === 'include' ||
|
|
626
861
|
relation === 'extend') {
|
|
627
862
|
markerEnd = 'open-arrow';
|
|
628
863
|
}
|
|
864
|
+
else if (relation === 'synchronous')
|
|
865
|
+
markerEnd = 'arrow';
|
|
629
866
|
}
|
|
630
867
|
if (!seen.has('stroke-style')) {
|
|
631
|
-
dashed = ['dependency', 'realization', 'include', 'extend'].includes(relation);
|
|
868
|
+
dashed = ['dependency', 'realization', 'return', 'include', 'extend'].includes(relation);
|
|
632
869
|
}
|
|
633
870
|
if (label === undefined && (relation === 'include' || relation === 'extend')) {
|
|
634
871
|
label = `«${relation}»`;
|
|
635
872
|
}
|
|
636
|
-
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
873
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
637
874
|
}
|
|
638
875
|
function parseConnection(match, line) {
|
|
639
876
|
const tail = splitDeclarationTail(match[5], line);
|
|
@@ -651,6 +888,10 @@ function parseConnection(match, line) {
|
|
|
651
888
|
};
|
|
652
889
|
if (options.label !== undefined)
|
|
653
890
|
connection.label = options.label;
|
|
891
|
+
if (options.signalKind !== undefined)
|
|
892
|
+
connection.signalKind = options.signalKind;
|
|
893
|
+
if (options.width !== undefined)
|
|
894
|
+
connection.width = options.width;
|
|
654
895
|
return connection;
|
|
655
896
|
}
|
|
656
897
|
function validateComponent(component, fence) {
|
|
@@ -675,6 +916,59 @@ function validGatePort(component, port) {
|
|
|
675
916
|
function isClassicalGate(component) {
|
|
676
917
|
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
677
918
|
}
|
|
919
|
+
function isDigitalComponent(component) {
|
|
920
|
+
return DIGITAL_COMPONENT_KINDS.includes(component.kind);
|
|
921
|
+
}
|
|
922
|
+
function isQuantumSpecial(component) {
|
|
923
|
+
return QUANTUM_SPECIAL_KINDS.includes(component.kind);
|
|
924
|
+
}
|
|
925
|
+
function validIndexedPort(port, inputs, outputs) {
|
|
926
|
+
if (port === 'in')
|
|
927
|
+
return inputs > 0;
|
|
928
|
+
if (port === 'out')
|
|
929
|
+
return outputs > 0;
|
|
930
|
+
const match = port.match(/^(in|out)([1-9]\d*)$/);
|
|
931
|
+
if (match === null)
|
|
932
|
+
return false;
|
|
933
|
+
const count = match[1] === 'in' ? inputs : outputs;
|
|
934
|
+
return Number(match[2]) <= count;
|
|
935
|
+
}
|
|
936
|
+
function validDigitalPort(component, port) {
|
|
937
|
+
if (validIndexedPort(port, component.inputs, component.outputs))
|
|
938
|
+
return true;
|
|
939
|
+
switch (component.kind) {
|
|
940
|
+
case 'buffer':
|
|
941
|
+
return component.variant?.startsWith('tristate') === true && port === 'enable';
|
|
942
|
+
case 'flipflop':
|
|
943
|
+
return ['d', 'j', 'k', 's', 'r', 't', 'clock', 'enable', 'preset', 'clear', 'q', 'nq'].includes(port);
|
|
944
|
+
case 'mux':
|
|
945
|
+
return port === 'select' || port === 'enable';
|
|
946
|
+
case 'register':
|
|
947
|
+
case 'counter':
|
|
948
|
+
return ['clock', 'enable', 'preset', 'clear'].includes(port);
|
|
949
|
+
case 'comparator':
|
|
950
|
+
return ['gt', 'eq', 'lt'].includes(port);
|
|
951
|
+
case 'bus':
|
|
952
|
+
return ['bus', 'tap'].includes(port);
|
|
953
|
+
default:
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function validQuantumSpecialPort(component, port) {
|
|
958
|
+
if (component.kind === 'prepare')
|
|
959
|
+
return port === 'out';
|
|
960
|
+
if (component.kind === 'control')
|
|
961
|
+
return ['in', 'out', 'control'].includes(port);
|
|
962
|
+
if (component.kind === 'measure')
|
|
963
|
+
return ['in', 'out', 'classical'].includes(port);
|
|
964
|
+
if (component.kind === 'classical-bit' || component.kind === 'classical-register') {
|
|
965
|
+
return port === 'in' || port === 'out';
|
|
966
|
+
}
|
|
967
|
+
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
968
|
+
return validIndexedPort(port, tracks, tracks) ||
|
|
969
|
+
(/^control[1-9]\d*$/.test(port) && Number(port.slice(7)) <= component.controls) ||
|
|
970
|
+
(/^target[1-9]\d*$/.test(port) && Number(port.slice(6)) <= component.targets);
|
|
971
|
+
}
|
|
678
972
|
function validIcPort(component, port) {
|
|
679
973
|
const sides = Object.values(component.pins);
|
|
680
974
|
if (port === 'in') {
|
|
@@ -697,6 +991,12 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
697
991
|
if (isClassicalGate(component)) {
|
|
698
992
|
valid = validGatePort(component, endpoint.port);
|
|
699
993
|
}
|
|
994
|
+
else if (isDigitalComponent(component)) {
|
|
995
|
+
valid = validDigitalPort(component, endpoint.port);
|
|
996
|
+
}
|
|
997
|
+
else if (isQuantumSpecial(component)) {
|
|
998
|
+
valid = validQuantumSpecialPort(component, endpoint.port);
|
|
999
|
+
}
|
|
700
1000
|
else if (includesValue(UML_COMPONENT_KINDS, component.kind)) {
|
|
701
1001
|
valid = ['in', 'out', 'left', 'right', 'top', 'bottom'].includes(endpoint.port);
|
|
702
1002
|
if (!valid && component.kind === 'lifeline') {
|
|
@@ -733,6 +1033,19 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
733
1033
|
case 'port':
|
|
734
1034
|
case 'hadamard':
|
|
735
1035
|
case 'qgate':
|
|
1036
|
+
case 'xgate':
|
|
1037
|
+
case 'ygate':
|
|
1038
|
+
case 'zgate':
|
|
1039
|
+
case 'sgate':
|
|
1040
|
+
case 'sdg':
|
|
1041
|
+
case 'tgate':
|
|
1042
|
+
case 'tdg':
|
|
1043
|
+
case 'sx':
|
|
1044
|
+
case 'phase':
|
|
1045
|
+
case 'rx':
|
|
1046
|
+
case 'ry':
|
|
1047
|
+
case 'rz':
|
|
1048
|
+
case 'ugate':
|
|
736
1049
|
valid = endpoint.port === 'in' || endpoint.port === 'out';
|
|
737
1050
|
break;
|
|
738
1051
|
case 'ground':
|
|
@@ -744,12 +1057,68 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
744
1057
|
case 'ic':
|
|
745
1058
|
valid = validIcPort(component, endpoint.port);
|
|
746
1059
|
break;
|
|
1060
|
+
case 'junction':
|
|
1061
|
+
case 'testpoint':
|
|
1062
|
+
valid = ['in', 'out', 'node'].includes(endpoint.port);
|
|
1063
|
+
break;
|
|
1064
|
+
case 'connector':
|
|
1065
|
+
valid = endpoint.port === 'in' || endpoint.port === 'out';
|
|
1066
|
+
break;
|
|
1067
|
+
case 'source':
|
|
1068
|
+
valid = ['in', 'out', 'positive', 'negative'].includes(endpoint.port) ||
|
|
1069
|
+
(['vcvs', 'vccs', 'ccvs', 'cccs'].includes(String(component.variant)) && ['control-positive', 'control-negative'].includes(endpoint.port));
|
|
1070
|
+
break;
|
|
1071
|
+
case 'power':
|
|
1072
|
+
valid = endpoint.port === 'in';
|
|
1073
|
+
break;
|
|
1074
|
+
case 'switch':
|
|
1075
|
+
valid = component.variant === 'spdt'
|
|
1076
|
+
? ['in', 'out', 'common', 'normally-open', 'normally-closed'].includes(endpoint.port)
|
|
1077
|
+
: component.variant === 'relay'
|
|
1078
|
+
? ['in', 'out', 'coil1', 'coil2'].includes(endpoint.port)
|
|
1079
|
+
: ['in', 'out'].includes(endpoint.port);
|
|
1080
|
+
break;
|
|
1081
|
+
case 'amplifier':
|
|
1082
|
+
valid = ['in', 'positive', 'negative', 'out', 'v+', 'v-'].includes(endpoint.port);
|
|
1083
|
+
break;
|
|
1084
|
+
case 'protection':
|
|
1085
|
+
case 'resonator':
|
|
1086
|
+
case 'meter':
|
|
1087
|
+
case 'load':
|
|
1088
|
+
valid = ['in', 'out'].includes(endpoint.port);
|
|
1089
|
+
break;
|
|
747
1090
|
}
|
|
748
1091
|
}
|
|
749
1092
|
if (!valid) {
|
|
750
1093
|
throw new SchematicSyntaxError(`Port ${endpoint.componentId}.${endpoint.port} is invalid for ${component.kind}.`, line);
|
|
751
1094
|
}
|
|
752
1095
|
}
|
|
1096
|
+
function endpointWidth(endpoint, components) {
|
|
1097
|
+
const component = components.get(endpoint.componentId);
|
|
1098
|
+
if (component.kind === 'port')
|
|
1099
|
+
return component.width ?? 1;
|
|
1100
|
+
if (component.kind === 'classical-register')
|
|
1101
|
+
return component.width;
|
|
1102
|
+
if (isDigitalComponent(component)) {
|
|
1103
|
+
if (component.kind === 'register' && (endpoint.port === 'in' || endpoint.port === 'out')) {
|
|
1104
|
+
return component.width;
|
|
1105
|
+
}
|
|
1106
|
+
if (component.kind === 'bus' && endpoint.port === 'bus')
|
|
1107
|
+
return component.width;
|
|
1108
|
+
}
|
|
1109
|
+
return 1;
|
|
1110
|
+
}
|
|
1111
|
+
function validateConnectionWidth(connection, components) {
|
|
1112
|
+
const sourceWidth = endpointWidth(connection.from, components);
|
|
1113
|
+
const targetWidth = endpointWidth(connection.to, components);
|
|
1114
|
+
const width = connection.width ?? 1;
|
|
1115
|
+
if ((sourceWidth > 1 || targetWidth > 1) && connection.width === undefined) {
|
|
1116
|
+
throw new SchematicSyntaxError('Bus connections require an explicit width option.', connection.line);
|
|
1117
|
+
}
|
|
1118
|
+
if (sourceWidth !== targetWidth || width !== sourceWidth) {
|
|
1119
|
+
throw new SchematicSyntaxError(`Connection width ${width} is incompatible with ${sourceWidth}-bit source and ${targetWidth}-bit target ports.`, connection.line);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
753
1122
|
export function parseSchematicFence(info, defaultTitle = 'Engineering schematic') {
|
|
754
1123
|
if (info === undefined || !/^schemd(?:\s|$)/i.test(info.trim())) {
|
|
755
1124
|
return undefined;
|
|
@@ -764,6 +1133,9 @@ export function parseSchematicFence(info, defaultTitle = 'Engineering schematic'
|
|
|
764
1133
|
throw new SchematicSyntaxError('Schematic bounds must be integers from 64 through 4096.');
|
|
765
1134
|
}
|
|
766
1135
|
const title = match[3] ?? defaultTitle;
|
|
1136
|
+
if (title.trim() === '') {
|
|
1137
|
+
throw new SchematicSyntaxError('Schematic titles cannot be empty.');
|
|
1138
|
+
}
|
|
767
1139
|
if (title.length > MAX_FENCE_TITLE_LENGTH) {
|
|
768
1140
|
throw new SchematicSyntaxError('Schematic titles cannot exceed 512 characters.');
|
|
769
1141
|
}
|
|
@@ -812,8 +1184,11 @@ export function parseSchematic(source, fence) {
|
|
|
812
1184
|
for (const connection of connections) {
|
|
813
1185
|
validateEndpoint(connection.from, componentsById, connection.line);
|
|
814
1186
|
validateEndpoint(connection.to, componentsById, connection.line);
|
|
1187
|
+
validateConnectionWidth(connection, componentsById);
|
|
815
1188
|
}
|
|
816
1189
|
const document = { components, connections };
|
|
817
|
-
validateDocumentGeometry(document, fence);
|
|
818
|
-
|
|
1190
|
+
const routes = validateDocumentGeometry(document, fence);
|
|
1191
|
+
const parsedDocument = freezeParsedDocument(document);
|
|
1192
|
+
cacheParsedSchematicRoutes(parsedDocument, fence.bounds, routes);
|
|
1193
|
+
return parsedDocument;
|
|
819
1194
|
}
|