diff --git a/src/slang/core/abstract/port-owner.ts b/src/slang/core/abstract/port-owner.ts index e07b79c8..63773c9b 100644 --- a/src/slang/core/abstract/port-owner.ts +++ b/src/slang/core/abstract/port-owner.ts @@ -13,6 +13,16 @@ export abstract class PortOwner extends SlangNode { this.streamPortOwner.initialize(); } + // tslint:disable-next-line:naming-convention + public get II(): PortModel { + return this.getPortIn()!; + } + + // tslint:disable-next-line:naming-convention + public get OO(): PortModel { + return this.getPortOut()!; + } + public getPortIn(): PortModel | null { return this.scanChildNode(GenericPortModel, (p) => p.isDirectionIn()) || null; } diff --git a/src/slang/core/abstract/port.ts b/src/slang/core/abstract/port.ts index b7b2dd12..0fd3173a 100644 --- a/src/slang/core/abstract/port.ts +++ b/src/slang/core/abstract/port.ts @@ -7,7 +7,7 @@ import {BlackBox} from "./blackbox"; import {SlangNode} from "./nodes"; import {PortOwner} from "./port-owner"; import {StreamPort} from "./stream"; -import {canConnectTo} from "./utils/connection-check"; +import {canConnectTo, typesCompatibleTo} from "./utils/connection-check"; import {Connections} from "./utils/connections"; import {SlangSubject} from "./utils/events"; import {GenericSpecifications} from "./utils/generics"; @@ -101,6 +101,18 @@ export abstract class GenericPortModel extends SlangNode { this.streamPort.initialize(); } + public get sub(): GenericPortModel { + return this.getStreamSub(); + } + + public get mapSubs(): Array> { + return Array.from(this.getMapSubs()); + } + + public map(name: string): GenericPortModel { + return this.findMapSub(name); + } + public reconstruct(type: SlangType, portCtor: new (p: GenericPortModel | O, args: PortModelArgs) => PortModel, direction: PortDirection, generic: boolean = false): void { /* * When generic == true then this method is called because this port is generic-like and its specification has changed @@ -293,52 +305,6 @@ export abstract class GenericPortModel extends SlangNode { return type; } - public anySubStreamConnected(): boolean { - if (this.connectedWith.length !== 0) { - return true; - } - - if (this.typeIdentifier === TypeIdentifier.Map) { - for (const sub of this.getMapSubs()) { - if (sub.anySubStreamConnected()) { - return true; - } - } - } else if (this.typeIdentifier === TypeIdentifier.Stream) { - return this.getStreamSub().anySubStreamConnected(); - } - - return false; - } - - public getConnectedType(): SlangType { - const type = new SlangType(null, this.typeIdentifier, true); - switch (this.typeIdentifier) { - case TypeIdentifier.Map: - for (const subPort of this.getMapSubs()) { - if (!subPort.anySubStreamConnected()) { - continue; - } - const subType = subPort.getConnectedType(); - if (!subType.isVoid()) { - type.addMapSub(subPort.getName(), subType); - } - } - break; - case TypeIdentifier.Stream: - type.setStreamSub(this.getStreamSub().getConnectedType()); - break; - case TypeIdentifier.Generic: - type.setGenericIdentifier(this.getGenericIdentifier()); - break; - default: - if (!this.anySubStreamConnected()) { - return new SlangType(null, TypeIdentifier.Unspecified, true); - } - } - return type; - } - public getTypeIdentifier(): TypeIdentifier { return this.typeIdentifier; } @@ -598,16 +564,6 @@ export abstract class GenericPortModel extends SlangNode { fetchedGenerics.specifications.unregisterPort(fetchedGenerics.identifier, this); }); - this.subscribeDisconnected(() => { - if (this.connectedWith.length !== 0) { - return; - } - const newType = specifications.getUnifiedType(identifier); - if (newType && !specifications.get(identifier).equals(newType)) { - specifications.specify(identifier, newType); - } - }); - if (!this.isGenericLike()) { return; } @@ -620,6 +576,13 @@ export abstract class GenericPortModel extends SlangNode { this.typeIdentifier = TypeIdentifier.Unspecified; this.genericIdentifier = undefined; } + for (const connection of this.getConnectionsTo()) { + const dest = connection.destination; + const src = connection.source; + if (!typesCompatibleTo(src.getType(), dest.getType())) { + src.disconnectTo(dest); + } + } }); } @@ -692,7 +655,7 @@ export abstract class GenericPortModel extends SlangNode { */ private connectTo(destination: PortModel, createGenerics: boolean) { if (!canConnectTo(this, destination, createGenerics)) { - throw new Error(`cannot connect: ${this.getIdentity()} --> ${destination.getIdentity()}`); + throw new Error(`cannot connect: ${this.getOwnerName()}:${this.getPortReference()} --> ${destination.getOwnerName()}:${destination.getPortReference()}`); } if ((createGenerics && this.isGenericLike()) || destination.isTrigger()) { this.connectDirectlyTo(destination, createGenerics); diff --git a/src/slang/core/abstract/utils/connection-check.ts b/src/slang/core/abstract/utils/connection-check.ts index c0b9b931..f04d9919 100644 --- a/src/slang/core/abstract/utils/connection-check.ts +++ b/src/slang/core/abstract/utils/connection-check.ts @@ -20,12 +20,16 @@ function typesMapCompatibleTo(mapTypeA: SlangType, mapTypeB: SlangType): boolean return true; } -function typesCompatibleTo(sourceType: SlangType, destinationType: SlangType): boolean { +export function typesCompatibleTo(sourceType: SlangType, destinationType: SlangType): boolean { // Triggers can always be destinations, even for specifications, maps and streams if (destinationType.getTypeIdentifier() === TypeIdentifier.Trigger) { return true; } + if (sourceType.getTypeIdentifier() === TypeIdentifier.Trigger) { + return true; + } + // Careful: destinationType.getTypeIdentifier() === TypeIdentifier.Primitive is not identical with destinationType.isPrimitive() // isPrimitive() is true for Strings, Numbers, etc. if (destinationType.getTypeIdentifier() === TypeIdentifier.Primitive && sourceType.isPrimitive()) { diff --git a/src/slang/core/abstract/utils/generics.ts b/src/slang/core/abstract/utils/generics.ts index 03c43367..cce9e443 100644 --- a/src/slang/core/abstract/utils/generics.ts +++ b/src/slang/core/abstract/utils/generics.ts @@ -72,18 +72,6 @@ export class GenericSpecifications { } } - public getUnifiedType(identifier: string): SlangType | null { - const portSet = this.ports.get(identifier); - if (!portSet) { - return null; - } - let unifiedType = this.get(identifier).getOnlyFixedSubs(); - for (const registeredPort of portSet) { - unifiedType = unifiedType.union(registeredPort.getConnectedType()); - } - return unifiedType; - } - public subscribeGenericTypeChanged(identifier: string, cb: (type: SlangType | null) => void): Subscription { return this.getSubject(identifier).subscribe(cb); } diff --git a/src/slang/core/models/blueprint.ts b/src/slang/core/models/blueprint.ts index 856686e5..67b6d82e 100644 --- a/src/slang/core/models/blueprint.ts +++ b/src/slang/core/models/blueprint.ts @@ -220,6 +220,11 @@ export class BlueprintModel extends BlackBox implements HasMoveablePortGroups { return this.createChildNode(BlueprintPortModel, args); } + public definePort(args: PortModelArgs): this { + this.createPort(args); + return this; + } + public getShortName(): string { return this.name; } diff --git a/src/slang/definitions/type.ts b/src/slang/definitions/type.ts index e874b3cf..7d1b86f7 100644 --- a/src/slang/definitions/type.ts +++ b/src/slang/definitions/type.ts @@ -139,10 +139,18 @@ export class SlangType { return SlangType.new(TypeIdentifier.Unspecified); } + public static newTrigger(): SlangType { + return SlangType.new(TypeIdentifier.Trigger); + } + public static newString(): SlangType { return SlangType.new(TypeIdentifier.String); } + public static newNumber(): SlangType { + return SlangType.new(TypeIdentifier.Number); + } + public static newBoolean(): SlangType { return SlangType.new(TypeIdentifier.Boolean); } @@ -151,16 +159,22 @@ export class SlangType { return SlangType.new(TypeIdentifier.Generic).setGenericIdentifier(identifier); } - public static newMap(): SlangType { - return SlangType.new(TypeIdentifier.Map); + public static newMap(mapEntries?: {[n: string]: SlangType}): SlangType { + const map = SlangType.new(TypeIdentifier.Map); + if (mapEntries) { + Object.entries(mapEntries).forEach(([name, subType]) => { + map.addMapSub(name, subType); + }); + } + return map; } - public static newStream(subTid?: TypeIdentifier): SlangType { - const strType = SlangType.new(TypeIdentifier.Stream); - if (!subTid) { - return strType; + public static newStream(sub?: TypeIdentifier|SlangType): SlangType { + const stream = SlangType.new(TypeIdentifier.Stream); + if (sub) { + stream.setStreamSub(sub instanceof SlangType ? sub : SlangType.new(sub)); } - return SlangType.new(TypeIdentifier.Stream).setStreamSub(SlangType.new(subTid)); + return stream; } private readonly mapSubs: Map | undefined; @@ -385,58 +399,6 @@ export class SlangType { return this.typeIdentifier; } - /** - * Returns true iff this type contains at least one element that is fixed (i.e. that has not been - * inferred). - */ - public hasAnyFixedSub(): boolean { - if (this.typeIdentifier === TypeIdentifier.Map) { - for (const sub of this.getMapSubs()) { - if (sub[1].hasAnyFixedSub()) { - return true; - } - } - } else if (this.typeIdentifier === TypeIdentifier.Stream) { - return this.getStreamSub().hasAnyFixedSub(); - } else if (!this.inferred) { - return true; - } - - return false; - } - - /** - * Returns a type which is part of this type but contains only fixed elements (i.e. elements that have not been - * inferred). - */ - public getOnlyFixedSubs(): SlangType { - const type = new SlangType(null, this.typeIdentifier); - switch (this.typeIdentifier) { - case TypeIdentifier.Map: - for (const sub of this.getMapSubs()) { - if (!sub[1].hasAnyFixedSub()) { - continue; - } - const subType = sub[1].getOnlyFixedSubs(); - if (!subType.isVoid()) { - type.addMapSub(sub[0], subType); - } - } - break; - case TypeIdentifier.Stream: - type.setStreamSub(this.getStreamSub().getOnlyFixedSubs()); - break; - case TypeIdentifier.Generic: - type.setGenericIdentifier(this.getGenericIdentifier()); - break; - default: - if (!this.hasAnyFixedSub()) { - return SlangType.newUnspecified(); - } - } - return type; - } - public isElementaryPort(): boolean { return this.isPrimitive() || this.isTrigger() || this.isGeneric(); } diff --git a/src/styles/index.ts b/src/styles/index.ts index 996b4397..8cb18cb6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1,3 +1 @@ -// @ts-ignore -import styling from "./index.scss"; -export const STYLING = styling.toString(); +export const STYLING: string = `@import url(https://fonts.googleapis.com/css?family=Roboto+Slab:300%7CRoboto);.joint-viewport{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.joint-paper-background,.joint-paper-grid,.joint-paper>svg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-element .scalable *{vector-effect:non-scaling-stroke}.marker-source,.marker-target{vector-effect:non-scaling-stroke}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-vertices{opacity:0;cursor:move}.marker-arrowheads{opacity:0;cursor:move;cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px 0 5px}.sl-port{-webkit-transition-duration:.2s;transition-duration:.2s}.sl-view{width:100%;height:100%}.sl-blackbox .sl-rectangle,.sl-blackbox-ghost .sl-rectangle{fill:#fefefe;stroke:#212124;stroke-width:1.5px;paint-order:stroke}.sl-blackbox .sl-label,.sl-blackbox-ghost .sl-label{fill:#212124}.sl-blackbox-ghost .sl-rectangle{fill-opacity:.1}.sl-outer .sl-rectangle{stroke:#212124;fill:#fefefe;stroke-width:1.5px}.sl-outer.sl-blupr-elem .sl-rectangle{cursor:not-allowed;fill:rgba(33,33,36,.1)}.sl-blueprint-port .sl-label{fill:#212124}.sl-port{stroke-width:0;stroke-opacity:.6;-webkit-transition-property:stroke-width;transition-property:stroke-width}.sl-port.sl-stripe{stroke:#fff;fill:#fff}.sl-port.sl-type-number{stroke:#2e49b3;fill:#2e49b3}.sl-port.sl-type-string{stroke:#952e2e;fill:#952e2e}.sl-port.sl-type-boolean{stroke:#ff764d;fill:#ff764d}.sl-port.sl-type-binary{stroke:#83a91d;fill:#83a91d}.sl-port.sl-type-primitive{stroke:#209cee;fill:#209cee}.sl-port.sl-type-trigger{stroke:rgba(152,152,151,.5);fill:rgba(152,152,151,.5)}.sl-port.sl-type-generic{stroke:#b86bff;fill:#b86bff}.sl-port.sl-type-ghost{stroke:rgba(184,107,255,.5);fill:rgba(184,107,255,.5)}.sl-connection-wrap{fill:none;stroke:#989897;stroke-width:10px;stroke-opacity:0;stroke-linecap:round}.sl-connection-wrap:hover{stroke-opacity:.3}.sl-connection-wrap.sl-is-selected{stroke:#212124;stroke-opacity:.2}.sl-connection-wrap.sl-is-selected~.link-tools{display:initial}.sl-connection{vector-effect:none;fill:none;stroke-opacity:1}.sl-connection.sl-type-number{stroke:#2e49b3}.sl-connection.sl-type-string{stroke:#952e2e}.sl-connection.sl-type-boolean{stroke:#ff764d}.sl-connection.sl-type-binary{stroke:#83a91d}.sl-connection.sl-type-primitive{stroke:#209cee}.sl-connection.sl-type-trigger{stroke:rgba(152,152,151,.5)}.sl-connection.sl-type-generic{stroke:#b86bff}.sl-connection.sl-type-ghost{stroke:rgba(184,107,255,.5);stroke-opacity:.3}@-webkit-keyframes port-pulse{from{stroke-width:0}to{stroke-width:8px}}@keyframes port-pulse{from{stroke-width:0}to{stroke-width:8px}}.joint-paper{font-family:Roboto,sans-serif;font-size:16px;font-size:1rem}.joint-highlight-stroke{stroke:none}.available-magnet path{-webkit-animation-name:port-pulse;animation-name:port-pulse;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-direction:alternate;animation-direction:alternate;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}.tool-remove circle{fill:#f74125}.tool-remove path{fill:#fefefe}.sl-port-info{background:#f5f5f5;padding:1px;margin:0;border-radius:8px;font-size:12px;font-size:.75rem}.sl-port-info .sl-port-name{text-decoration:underline;padding:0 6px}.sl-port-info .sl-port-type{color:#fafafa;padding:0 6px;border-radius:15px}.sl-port-info .sl-port-type.sl-type-number{background-color:#2e49b3}.sl-port-info .sl-port-type.sl-type-string{background-color:#952e2e}.sl-port-info .sl-port-type.sl-type-boolean{background-color:#ff764d}.sl-port-info .sl-port-type.sl-type-binary{background-color:#83a91d}.sl-port-info .sl-port-type.sl-type-primitive{background-color:#209cee}.sl-port-info .sl-port-type.sl-type-trigger{background-color:rgba(152,152,151,.5)}.sl-port-info .sl-port-type.sl-type-generic{background-color:#b86bff}.sl-port-info .sl-port-type.sl-type-ghost{background-color:rgba(184,107,255,.5)}`; diff --git a/test/behavior/stream.test.ts b/test/behavior/stream.test.ts index 68fd2689..7f0b4e5d 100644 --- a/test/behavior/stream.test.ts +++ b/test/behavior/stream.test.ts @@ -2,9 +2,8 @@ import uuidv4 from "uuid/v4"; import {PortDirection} from "../../src/slang/core/abstract/port"; import {StreamType} from "../../src/slang/core/abstract/stream"; -import {AppModel} from "../../src/slang/core/models/app"; +import {AppModel, LandscapeModel} from "../../src/slang/core/models"; import {BlueprintType} from "../../src/slang/core/models/blueprint"; -import {LandscapeModel} from "../../src/slang/core/models/landscape"; import {SlangType, TypeIdentifier} from "../../src/slang/definitions/type"; import {TestStorageApp} from "../helpers/TestStorageApp"; import data from "../resources/definitions.json"; @@ -61,53 +60,82 @@ describe("A stream port", () => { type: BlueprintType.Local, }); - const opGIS = bp.createBlankOperator(landscapeModel.findBlueprint("10a6eea5-4d5b-43b6-9106-6820d1009e3b")!); - const opG2G = bp.createBlankOperator(landscapeModel.findBlueprint("dc1aa556-d62e-4e07-adbb-53dc317481b0")!); - - opG2G.getGenerics().specify("itemType", SlangType.new(TypeIdentifier.Number)); - - const g2gOut = opG2G.getPortOut()!; - const gisInSub = opGIS.getPortIn()!.getStreamSub(); - - expect(g2gOut.canConnect(gisInSub)).toEqual(true); - g2gOut.connect(gisInSub, true); - expect(g2gOut.isConnectedWith(gisInSub.getMapSubs().next().value)).toEqual(true); - }); - - it("groups streams correctly", () => { - const bp = landscapeModel.createBlueprint({ - uuid: uuidv4(), - meta: {name: "test-bp-2"}, - type: BlueprintType.Local, - }); - const bpIn = bp.createPort({ - name: "", - type: new SlangType(null, TypeIdentifier.Number), - direction: PortDirection.In, - }); - const bpOut = bp.createPort({ - name: "", - type: new SlangType(null, TypeIdentifier.Unspecified), - direction: PortDirection.Out, - }); + const oGenericInStream = bp.createBlankOperator(landscapeModel.findBlueprint("10a6eea5-4d5b-43b6-9106-6820d1009e3b")!); + const oGenericToGeneric = bp.createBlankOperator(landscapeModel.findBlueprint("dc1aa556-d62e-4e07-adbb-53dc317481b0")!); - bpIn.connect(bpOut, true); + oGenericToGeneric.getGenerics().specify("itemType", SlangType.new(TypeIdentifier.Number)); - const op2OS = bp.createBlankOperator(landscapeModel.findBlueprint("b444f701-59fc-43a8-8cdc-8bcce9dd471d")!); - const opG2G = bp.createBlankOperator(landscapeModel.findBlueprint("dc1aa556-d62e-4e07-adbb-53dc317481b0")!); + expect(oGenericToGeneric.OO.canConnect(oGenericInStream.II.sub)).toBeTruthy(); - const osOut = op2OS.getPortOut()!; - const g2gIn = opG2G.getPortIn()!; - const g2gOut = opG2G.getPortOut()!; + oGenericToGeneric.OO.connect(oGenericInStream.II.sub, true); - osOut.getStreamSub().findMapSub("portA").connect(g2gIn, true); - osOut.getStreamSub().findMapSub("portB").connect(g2gIn, true); - Array.from(g2gOut.getMapSubs()).forEach((port) => { - port.connect(bpOut, true); - }); + expect(oGenericToGeneric.OO.isConnectedWith(oGenericInStream.II.sub.getMapSubs().next().value)).toBeTruthy(); + }); - expect(Array.from(bpOut.getMapSubs()).length).toEqual(2); - expect(Array.from(Array.from(bpOut.getMapSubs()).find((port) => port.getType().isStream())!.getStreamSub().getMapSubs()).length).toEqual(2); + it("connecting 2 stream ports with a generic port will create 2 stream ports within the generic port", () => { + const bp = landscapeModel + .createBlueprint({ + uuid: uuidv4(), + meta: {name: "test-bp-2"}, + type: BlueprintType.Local, + }) + .definePort({ + name: "", + type: SlangType.newNumber(), + direction: PortDirection.In, + }) + .definePort({ + name: "", + type: SlangType.newUnspecified(), + direction: PortDirection.Out, + }); + const bpOutStreamMap = landscapeModel + .createBlueprint({ + uuid: uuidv4(), + meta: {name: "OutStreamMap"}, + type: BlueprintType.Local, + }) + .definePort({ + name: "", + type: SlangType.newTrigger(), + direction: PortDirection.In, + }) + .definePort({ + name: "", + type: SlangType.newStream(SlangType.newMap({ + A: SlangType.newString(), + B: SlangType.newNumber(), + })), + direction: PortDirection.Out, + }); + const bpGenericToGeneric = landscapeModel + .createBlueprint({ + uuid: uuidv4(), + meta: {name: "GenericToGeneric"}, + type: BlueprintType.Local, + }) + .definePort({ + name: "", + type: SlangType.newGeneric("itemType"), + direction: PortDirection.In, + }) + .definePort({ + name: "", + type: SlangType.newGeneric("itemType"), + direction: PortDirection.Out, + }); + + const oOutStreamMap = bp.createBlankOperator(bpOutStreamMap); + const oGenericToGeneric = bp.createBlankOperator(bpGenericToGeneric); + + oOutStreamMap.OO.sub.map("A").connect(oGenericToGeneric.II, true); + oOutStreamMap.OO.sub.map("B").connect(oGenericToGeneric.II, true); + + oGenericToGeneric.OO.map("A").connect(bp.OO, true); + oGenericToGeneric.OO.map("B").connect(bp.OO, true); + + expect(bp.OO.mapSubs.length).toEqual(2); + expect(Array.from(Array.from(bp.OO.getMapSubs()).find((port) => port.getType().isStream())!.getStreamSub().getMapSubs()).length).toEqual(2); }); it("groups streams correctly after deletion", () => {