@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/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_-]*)\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+(.+)$/;
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(`${name} must be one of: ${values.join(', ')}.`, line);
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
- 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) };
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
- 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
+ }
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
- const quantumKind = kind;
491
- assertOnlyAttributes(attributes, quantumKind === 'qgate' ? ['parameter', 'matrix', 'phase'] : [], line);
492
- const component = { kind: quantumKind, ...common };
493
- const parameter = attributes.get('parameter');
494
- const matrix = attributes.get('matrix');
495
- const phase = attributes.get('phase');
496
- if (parameter !== undefined)
497
- component.parameter = parameter;
498
- if (matrix !== undefined)
499
- component.matrix = matrix;
500
- if (phase !== undefined)
501
- component.phase = phase;
502
- 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);
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
- return freezeParsedDocument(document);
1190
+ const routes = validateDocumentGeometry(document, fence);
1191
+ const parsedDocument = freezeParsedDocument(document);
1192
+ cacheParsedSchematicRoutes(parsedDocument, fence.bounds, routes);
1193
+ return parsedDocument;
819
1194
  }