@schemd/core 0.2.1 → 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/dist/parser.js CHANGED
@@ -1,10 +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
4
  import { cacheParsedSchematicRoutes } from './route-cache.js';
5
5
  import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
6
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+(.+)$/;
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
+ 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+(.+)$/;
8
8
  const SCHEMD_FENCE_PATTERN = /^schemd\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
9
9
  const NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
10
10
  const ANGLE_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:deg|grad|rad|turn)?$/i;
@@ -32,7 +32,11 @@ function freezeParsedDocument(document) {
32
32
  Object.freeze(component.pins.bottom);
33
33
  Object.freeze(component.pins);
34
34
  }
35
- 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') {
36
40
  Object.freeze(component.attributes);
37
41
  Object.freeze(component.operations);
38
42
  }
@@ -275,10 +279,26 @@ function parseEnumOption(value, fallback, values, name, line) {
275
279
  if (value === undefined)
276
280
  return fallback;
277
281
  if (!includesValue(values, value)) {
278
- throw new SchematicSyntaxError(`${name} must be one of: ${values.join(', ')}.`, line);
282
+ throw new SchematicSyntaxError(`Option ${name} must be one of: ${values.join(', ')}.`, line);
279
283
  }
280
284
  return value;
281
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
+ }
282
302
  function parseIcPins(attributes, line) {
283
303
  const registered = new Set();
284
304
  const parseSide = (side) => {
@@ -372,39 +392,81 @@ function parseComponent(match, line) {
372
392
  const attributes = parseAttributes(tail.options, line);
373
393
  const common = commonComponent(match, tail.color === '' ? 'slate' : tail.color, line);
374
394
  if (includesValue(PASSIVE_KINDS, kind)) {
375
- assertOnlyAttributes(attributes, [], line);
376
- return { kind, ...common };
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) };
377
405
  }
378
406
  if (includesValue(ANALOG_KINDS, kind)) {
379
407
  switch (kind) {
380
408
  case 'diode':
381
- assertOnlyAttributes(attributes, ['type'], line);
409
+ assertOnlyAttributes(attributes, ['type', 'orientation'], line);
382
410
  return {
383
411
  kind,
384
412
  ...common,
385
- 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)
386
415
  };
387
416
  case 'transistor':
388
- assertOnlyAttributes(attributes, ['type'], line);
417
+ assertOnlyAttributes(attributes, ['type', 'orientation'], line);
389
418
  return {
390
419
  kind,
391
420
  ...common,
392
- 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)
393
423
  };
394
- case 'port':
395
- assertOnlyAttributes(attributes, [], line);
396
- return { kind, ...common };
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
+ }
397
431
  case 'ground':
398
- assertOnlyAttributes(attributes, ['style'], line);
432
+ assertOnlyAttributes(attributes, ['style', 'orientation'], line);
399
433
  return {
400
434
  kind,
401
435
  ...common,
402
- 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)
403
438
  };
404
439
  }
405
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
+ }
406
468
  if (includesValue(CLASSICAL_GATE_KINDS, kind)) {
407
- assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard'], line);
469
+ assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard', 'orientation'], line);
408
470
  const standard = attributes.get('standard') ?? 'ieee';
409
471
  if (standard !== 'ieee' && standard !== 'iec') {
410
472
  throw new SchematicSyntaxError('standard must be ieee or iec.', line);
@@ -414,26 +476,93 @@ function parseComponent(match, line) {
414
476
  ...common,
415
477
  inputs: parseCount(attributes.get('inputs'), kind === 'not' ? 1 : 2, 'inputs', line),
416
478
  outputs: parseCount(attributes.get('outputs'), 1, 'outputs', line),
417
- standard
479
+ standard,
480
+ ...parseOrientation(attributes, line)
418
481
  };
419
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)
538
+ };
539
+ if (variant !== undefined)
540
+ digital.variant = variant;
541
+ return digital;
542
+ }
420
543
  if (kind === 'ic') {
421
- assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom'], line);
544
+ assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom', 'orientation'], line);
422
545
  const pins = parseIcPins(attributes, line);
423
546
  return {
424
547
  kind,
425
548
  ...common,
426
549
  pins,
427
- ...integratedCircuitDimensions(pins)
550
+ ...integratedCircuitDimensions(pins),
551
+ ...parseOrientation(attributes, line)
428
552
  };
429
553
  }
430
554
  if (includesValue(UML_COMPONENT_KINDS, kind)) {
431
555
  switch (kind) {
432
- case 'class': {
556
+ case 'class':
557
+ case 'interface':
558
+ case 'enumeration':
559
+ case 'datatype':
560
+ case 'object': {
433
561
  assertOnlyAttributes(attributes, ['attributes', 'operations', 'stereotype', 'width'], line);
434
562
  const classAttributes = parseUmlRows(attributes.get('attributes'), 'attributes', line);
435
563
  const operations = parseUmlRows(attributes.get('operations'), 'operations', line);
436
- const stereotype = attributes.get('stereotype');
564
+ const stereotype = attributes.get('stereotype') ??
565
+ (kind === 'interface' || kind === 'enumeration' || kind === 'datatype' ? kind : undefined);
437
566
  if (stereotype !== undefined && (stereotype === '' || stereotype.length > 128)) {
438
567
  throw new SchematicSyntaxError('stereotype must contain 1 through 128 characters.', line);
439
568
  }
@@ -467,13 +596,33 @@ function parseComponent(match, line) {
467
596
  bodyHeight: 40 + details.length * UML_ROW_HEIGHT
468
597
  };
469
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
+ };
470
606
  case 'usecase':
471
607
  case 'lifeline':
472
608
  case 'note':
473
- 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': {
474
623
  assertOnlyAttributes(attributes, ['width', 'height'], line);
475
624
  const defaultWidth = Math.max(kind === 'usecase' ? 112 : 96, mathLabelTextWidth(common.label, 8) + 28);
476
- const defaultHeight = kind === 'lifeline' ? 180 : kind === 'usecase' ? 56 : 64;
625
+ const defaultHeight = kind === 'lifeline' ? 180 : kind === 'activation' ? 96 : kind === 'usecase' ? 56 : 64;
477
626
  return {
478
627
  kind,
479
628
  ...common,
@@ -482,25 +631,87 @@ function parseComponent(match, line) {
482
631
  };
483
632
  }
484
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':
485
654
  case 'initial':
486
655
  case 'final':
487
656
  assertOnlyAttributes(attributes, [], line);
488
657
  return { kind, ...common };
489
658
  }
490
659
  }
491
- const quantumKind = kind;
492
- assertOnlyAttributes(attributes, quantumKind === 'qgate' ? ['parameter', 'matrix', 'phase'] : [], line);
493
- const component = { kind: quantumKind, ...common };
494
- const parameter = attributes.get('parameter');
495
- const matrix = attributes.get('matrix');
496
- const phase = attributes.get('phase');
497
- if (parameter !== undefined)
498
- component.parameter = parameter;
499
- if (matrix !== undefined)
500
- component.matrix = matrix;
501
- if (phase !== undefined)
502
- component.phase = phase;
503
- return component;
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);
504
715
  }
505
716
  function parseEndpoint(componentId, port) {
506
717
  return { componentId, port };
@@ -538,8 +749,10 @@ function parseConnectionOptions(raw, line) {
538
749
  let relation = 'signal';
539
750
  let label;
540
751
  let dashed = false;
752
+ let signalKind;
753
+ let width;
541
754
  if (raw === undefined || raw.trim() === '') {
542
- return { curve, markerStart, markerEnd, relation, label, dashed };
755
+ return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
543
756
  }
544
757
  const seen = new Set();
545
758
  for (const token of connectionOptionTokens(raw.trim(), line)) {
@@ -567,6 +780,14 @@ function parseConnectionOptions(raw, line) {
567
780
  seen.add('relation');
568
781
  continue;
569
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
+ }
570
791
  if (token === 'arrow' || token === 'dot') {
571
792
  if (seen.has('marker-end')) {
572
793
  throw new SchematicSyntaxError('Connection marker-end can only be declared once.', line);
@@ -575,7 +796,7 @@ function parseConnectionOptions(raw, line) {
575
796
  seen.add('marker-end');
576
797
  continue;
577
798
  }
578
- const match = token.match(/^(marker-start|marker-end|relation|label)=(.*)$/);
799
+ const match = token.match(/^(marker-start|marker-end|relation|label|signal|width)=(.*)$/);
579
800
  if (!match) {
580
801
  throw new SchematicSyntaxError('Unsupported connection routing, marker, relation, label, or stroke option.', line);
581
802
  }
@@ -602,6 +823,15 @@ function parseConnectionOptions(raw, line) {
602
823
  }
603
824
  label = optionValue;
604
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
+ }
605
835
  else {
606
836
  const marker = parseSignalMarker(optionValue, option, line);
607
837
  if (option === 'marker-start')
@@ -622,19 +852,25 @@ function parseConnectionOptions(raw, line) {
622
852
  markerEnd = 'triangle';
623
853
  else if (relation === 'dependency' ||
624
854
  relation === 'message' ||
855
+ relation === 'asynchronous' ||
856
+ relation === 'return' ||
857
+ relation === 'control-flow' ||
858
+ relation === 'object-flow' ||
625
859
  relation === 'transition' ||
626
860
  relation === 'include' ||
627
861
  relation === 'extend') {
628
862
  markerEnd = 'open-arrow';
629
863
  }
864
+ else if (relation === 'synchronous')
865
+ markerEnd = 'arrow';
630
866
  }
631
867
  if (!seen.has('stroke-style')) {
632
- dashed = ['dependency', 'realization', 'include', 'extend'].includes(relation);
868
+ dashed = ['dependency', 'realization', 'return', 'include', 'extend'].includes(relation);
633
869
  }
634
870
  if (label === undefined && (relation === 'include' || relation === 'extend')) {
635
871
  label = `«${relation}»`;
636
872
  }
637
- return { curve, markerStart, markerEnd, relation, label, dashed };
873
+ return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
638
874
  }
639
875
  function parseConnection(match, line) {
640
876
  const tail = splitDeclarationTail(match[5], line);
@@ -652,6 +888,10 @@ function parseConnection(match, line) {
652
888
  };
653
889
  if (options.label !== undefined)
654
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;
655
895
  return connection;
656
896
  }
657
897
  function validateComponent(component, fence) {
@@ -676,6 +916,59 @@ function validGatePort(component, port) {
676
916
  function isClassicalGate(component) {
677
917
  return CLASSICAL_GATE_KINDS.includes(component.kind);
678
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
+ }
679
972
  function validIcPort(component, port) {
680
973
  const sides = Object.values(component.pins);
681
974
  if (port === 'in') {
@@ -698,6 +991,12 @@ function validateEndpoint(endpoint, components, line) {
698
991
  if (isClassicalGate(component)) {
699
992
  valid = validGatePort(component, endpoint.port);
700
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
+ }
701
1000
  else if (includesValue(UML_COMPONENT_KINDS, component.kind)) {
702
1001
  valid = ['in', 'out', 'left', 'right', 'top', 'bottom'].includes(endpoint.port);
703
1002
  if (!valid && component.kind === 'lifeline') {
@@ -734,6 +1033,19 @@ function validateEndpoint(endpoint, components, line) {
734
1033
  case 'port':
735
1034
  case 'hadamard':
736
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':
737
1049
  valid = endpoint.port === 'in' || endpoint.port === 'out';
738
1050
  break;
739
1051
  case 'ground':
@@ -745,12 +1057,68 @@ function validateEndpoint(endpoint, components, line) {
745
1057
  case 'ic':
746
1058
  valid = validIcPort(component, endpoint.port);
747
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;
748
1090
  }
749
1091
  }
750
1092
  if (!valid) {
751
1093
  throw new SchematicSyntaxError(`Port ${endpoint.componentId}.${endpoint.port} is invalid for ${component.kind}.`, line);
752
1094
  }
753
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
+ }
754
1122
  export function parseSchematicFence(info, defaultTitle = 'Engineering schematic') {
755
1123
  if (info === undefined || !/^schemd(?:\s|$)/i.test(info.trim())) {
756
1124
  return undefined;
@@ -816,6 +1184,7 @@ export function parseSchematic(source, fence) {
816
1184
  for (const connection of connections) {
817
1185
  validateEndpoint(connection.from, componentsById, connection.line);
818
1186
  validateEndpoint(connection.to, componentsById, connection.line);
1187
+ validateConnectionWidth(connection, componentsById);
819
1188
  }
820
1189
  const document = { components, connections };
821
1190
  const routes = validateDocumentGeometry(document, fence);