import { Environment, ValueType, Expression, Pattern } from './types'; import * as _ from 'lodash'; import * as utils from './utils.js'; import { parseExpression, parsePattern } from './colorExpressionsParser'; type TokenMap = { [placeKey: string]: Token[] }; type PlaceExtensions = { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; type PlaceObject = { key: string; displayName: string; extensions: PlaceExtensions; } class Place { public readonly key: string; public readonly displayName: string; public readonly extensions: PlaceExtensions; constructor(key: string, displayName: string, extensions: PlaceExtensions) { this.key = key; this.displayName = displayName; this.extensions = extensions; } static fromObject(net: PetriNet, data: PlaceObject): Place { return new Place(data.key, data.displayName, data.extensions); } toObject(): PlaceObject { return { key: this.key, displayName: this.displayName, extensions: this.extensions }; } } const pullConsumedTokens = function (tokenMap: TokenMap, consumedTokens: TokenMap, deepEquality = false): boolean { // removes all tokens // returns true if successful // returns false (and clobbers tokenMap) if tokens are missing return _.every(_.toPairs(consumedTokens), ([ placeKey, tokens ]) => _.every(tokens, (token) => { const index = deepEquality ? _.findIndex(tokenMap[placeKey], (t) => _.isEqual(t, token)) : _.indexOf(tokenMap[placeKey], token); if (index < 0) return false; _.pullAt(tokenMap[placeKey], index); if (tokenMap[placeKey].length === 0) _.unset(tokenMap, placeKey); return true; })); }; type TokenObject = { readonly color: ValueType; } class Token { public readonly color: ValueType; constructor(color: ValueType) { this.color = color; } static fromObject(data: TokenObject): Token { return new Token(data.color); } toObject(): TokenObject { return { color: this.color }; } } type ConcurrentMoveObject = { readonly marking: MarkingObject; readonly moves: MoveObject[]; } class ConcurrentMove { public readonly net: PetriNet; public readonly marking: Marking; public readonly moves: Move[]; constructor(net: PetriNet, marking: Marking, moves: Move[]) { this.net = net; this.marking = marking; this.moves = moves; } toObject(): ConcurrentMoveObject { return { marking: this.marking.toObject(), moves: _.map([...this.moves], (move) => move.toObject()) }; } } type MoveObject = { readonly marking: MarkingObject | null; readonly transition: string; readonly consumedTokens: { [placeKey: string]: TokenObject[] }; readonly environment: Environment; readonly producedTokens: { [placeKey: string]: TokenObject[] }; } class Move { public readonly net: PetriNet; public readonly marking: Marking | null; public readonly transition: Transition; public readonly consumedTokens: { [placeKey: string]: Token[] }; public readonly environment: Environment; public readonly producedTokens: { [placeKey: string]: Token[] }; constructor(net: PetriNet, marking: Marking | null, transition: Transition, consumedTokens: { [placeKey: string]: Token[] }, environment: Environment, producedTokens: { [placeKey: string]: Token[] }) { this.net = net; this.marking = marking; this.transition = transition; this.consumedTokens = consumedTokens; this.environment = environment; this.producedTokens = producedTokens; } toObject(): MoveObject { return { marking: this.marking?.toObject() ?? null, transition: this.transition.key, consumedTokens: _.mapValues(this.consumedTokens, (tokenList) => _.map(tokenList, (token) => token.toObject())), environment: this.environment, producedTokens: _.mapValues(this.producedTokens, (tokenList) => _.map(tokenList, (token) => token.toObject())) }; } } type MarkingExtensions = { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; type MarkingObject = { readonly tokens: { [placeKey: string]: TokenObject[] }; readonly extensions: MarkingExtensions; } class Marking { public readonly net: PetriNet; public readonly tokens: TokenMap; public readonly extensions: MarkingExtensions; public readonly previousMarking: Marking | null; constructor(net: PetriNet, tokens: TokenMap, extensions: MarkingExtensions, previousMarking: Marking | null) { this.net = net; this.tokens = tokens; this.extensions = extensions; this.previousMarking = previousMarking; } static fromObject(net: PetriNet, data: MarkingObject): Marking { return new Marking(net, _.mapValues(data.tokens, (tokenList) => _.map(tokenList, (token) => Token.fromObject(token))), data.extensions, null); } toObject(): MarkingObject { return { tokens: _.mapValues(this.tokens, (tokenList) => _.map(tokenList, (token) => token.toObject())), extensions: this.extensions }; } /** * Returns something equivalent to `JSON.stringify(toObject())`, but guarantees that two equivalent markings return an identical string. * This is done by ensuring keys are sorted. * **Careful:** This only supports extensions that have [[TokenValue]]-like types. */ toStableJSON(): string { const thisObject = this.toObject(); const serializeToken = (token: TokenObject): string => `{"color":${utils.simpleToStableJSON(token.color)}}`; const serializeTokens = (tokens: TokenObject[]): string => `[${tokens.map(serializeToken).sort()}]`; const tokenBinders = Object.keys(thisObject.tokens).sort().map((k) => JSON.stringify(k) + ":" + serializeTokens(thisObject.tokens[k])); return `{"tokens":{${tokenBinders}},"extensions":${JSON.stringify(thisObject.extensions)}}`; } cloneTokens(): TokenMap { // returns a clone of the tokens list (with new lists, but shallow-copied tokens) return _.mapValues(this.tokens, _.clone); } enabledMoves(options?: { ignoreInFlowTokenCounts: boolean }): Move[] { // calculates the possible moves from this marking return _.flatMap(this.net.transitions, (transition) => { return transition.enabledMovesForMarking(this, options); }); } enabledConcurrentMoves(options: { ignoreInFlowTokenCounts: boolean }): ConcurrentMove[] { const movesPerTransition: (Move | null)[][] = _.map(this.net.transitions, (transition) => { return transition.enabledMovesForMarking(this, options); }); _.each(movesPerTransition, (movesList) => movesList.push(null)); // add option to not use a transition const possibleCombinations = utils.crossProduct(movesPerTransition); const res: ConcurrentMove[] = []; possibleCombinations.forEach((combination) => { const moves = _.filter(combination) as Move[]; // drop non-moves if (!moves.length) return; // drop empty combination const newTokenMap = this.cloneTokens(); // remove consumed tokens const success = _.every(moves, (move) => pullConsumedTokens(newTokenMap, move.consumedTokens, true)); // "true" to allow reading another, indistinguishable token if the selected token was already grabbed by a previously processed move if (!success) return; // not enough tokens // add produced tokens _.each(moves, (move) => _.each(_.toPairs(move.producedTokens), ([ placeKey, tokens ]) => _.each(tokens, (token) => { if (!newTokenMap[placeKey]) newTokenMap[placeKey] = []; newTokenMap[placeKey].push(token); }))); res.push(new ConcurrentMove(this.net, new Marking(this.net, newTokenMap, this.extensions, this), moves)); }); return res; } } type TransitionExtensions = { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; type InFlow = { readonly source: Place; readonly pattern: string | null; readonly compiledPattern: Pattern | null; }; type InFlowObject = { source: string; pattern: string | null; }; type InFlowIntermediate = { readonly source: Place; readonly pattern: string | null; }; type OutFlow = { readonly target: Place; readonly expression: string | null; readonly compiledExpression: Expression | null; } type OutFlowObject = { target: string; expression: string | null; } type OutFlowIntermediate = { readonly target: Place; readonly expression: string | null; } type TransitionObject = { key: string; displayName: string; guard: string | null; inFlows: InFlowObject[]; outFlows: OutFlowObject[]; extensions: TransitionExtensions; } class Transition { private static _registeredTransitionTypes: { [key: string]: { type: typeof Transition } } = {}; public static get registeredTransitionTypes(): { [key: string]: { type: typeof Transition } } { return this._registeredTransitionTypes; } static readonly registerTransitionType = (name: string, type: typeof Transition): void => { if (Transition._registeredTransitionTypes[name]) throw new Error("The type you were trying to register is already registered."); Transition._registeredTransitionTypes[name] = { type }; }; public readonly key: string; public readonly displayName: string; public readonly guard: string | null; private readonly compiledGuard: Expression | null; public readonly inFlows: InFlow[]; public readonly outFlows: OutFlow[]; public readonly extensions: TransitionExtensions; constructor(key: string, displayName: string, guard: string | null, inFlows: InFlowIntermediate[], outFlows: OutFlowIntermediate[], extensions: TransitionExtensions) { this.key = key; this.displayName = displayName; this.guard = guard; this.compiledGuard = guard == null ? null : parseExpression(guard); this.inFlows = _.map(inFlows, (inFlow) => ({ source: inFlow.source, pattern: inFlow.pattern, compiledPattern: inFlow.pattern == null ? null : parsePattern(inFlow.pattern) })); this.outFlows = _.map(outFlows, (outFlow) => ({ target: outFlow.target, expression: outFlow.expression, compiledExpression: outFlow.expression == null ? null : parseExpression(outFlow.expression) })); this.extensions = extensions; } static fromObject(net: PetriNet, data: TransitionObject): Transition { if (data.extensions.type) { const typeData = Transition.registeredTransitionTypes[data.extensions.type]; if (!typeData) throw new Error("The type of the transition you are trying to deserialize is unknown."); return typeData.type.fromObject(net, data); } return new Transition(data.key, data.displayName, data.guard, _.map(data.inFlows, (inFlow) => { const source = net.places[inFlow.source]; if (!source) throw new Error(`Source ${inFlow.source} not found while hydrating transition ${data.key}`); return { source, pattern: inFlow.pattern }; }), _.map(data.outFlows, (outFlow) => { const target = net.places[outFlow.target]; if (!target) throw new Error(`Target ${outFlow.target} not found while hydrating transition ${data.key}`); return { target, expression: outFlow.expression }; }), data.extensions); } inFlowsAsObject(): InFlowObject[] { return _.map(this.inFlows, (inFlow) => ({ source: inFlow.source.key, pattern: inFlow.pattern })); } outFlowsAsObject(): OutFlowObject[] { return _.map(this.outFlows, (outFlow) => ({ target: outFlow.target.key, expression: outFlow.expression })); } toObject(): TransitionObject { return { key: this.key, displayName: this.displayName, guard: this.guard, inFlows: this.inFlowsAsObject(), outFlows: this.outFlowsAsObject(), extensions: this.extensions }; } enabledMovesForMarking(marking: Marking, options?: { ignoreInFlowTokenCounts: boolean }): Move[] { // calculate the possible moves using this transition options = _.merge({ ignoreInFlowTokenCounts: false }, options); // first, for each inFlow, calculate a list of possible token reads const tokenReadsPerFlow = _.map(this.inFlows, (inFlow) => ( _.flatMap(marking.tokens[inFlow.source.key], (token) => { if (inFlow.compiledPattern == null) throw new Error("Nets with missing patterns have no direct semantics!"); const environments = inFlow.compiledPattern(token.color); return _.map(environments, (environment) => ({ place: inFlow.source, token, environment })); }) )); // we now have a list of objects containing an inFlow and every possible token read for it // combine them to produce every possible set of token reads // a "token read" is a way to read one token from each inFlow let tokenReads = utils.crossProduct(tokenReadsPerFlow); // eliminate reads that pull multiple copies of the same token if (!options.ignoreInFlowTokenCounts) { tokenReads = _.filter(tokenReads, (tokenRead) => { const readTokens = _.map(tokenRead, 'token'); return readTokens.length === _.uniq(readTokens).length; }); } // ensure that when there are duplicate tokens, only one of the equal combinations is processed tokenReads = _.uniqWith(tokenReads, _.isEqual); // next, we must filter the reads to the ones that produce a consistent environment, and calculate these environments const consistentTokenReads = _.flatMap(tokenReads, (tokenRead) => { const environment = utils.mergeEnvironments(...(_.map(tokenRead, ({ environment: partialEnvironment }) => partialEnvironment))); if (!environment) return []; // found inconsistency const consumedTokens = _.map(tokenRead, ({ place, token: consumedToken }) => ({ place, token: consumedToken })); return { environment, consumedTokens }; }); // now, we can drop reads that violate the guard const legalTokenReads = _.filter(consistentTokenReads, (tokenRead) => { if (this.compiledGuard == null) throw new Error("Nets with missing guards have no direct semantics!"); return _.some(this.compiledGuard(tokenRead.environment), (value) => value); }); // finally, we need to calculate the outFlows return _.flatMap(legalTokenReads, (tokenRead) => { // calculate all valid moves on this transition using these token reads const environment = tokenRead.environment; // calculate all possible writes for each outFlow const tokenWritesPerFlow = _.map(this.outFlows, (outFlow) => { if (outFlow.compiledExpression == null) throw new Error("Nets with missing expressions have no direct semantics!"); return _.map(outFlow.compiledExpression(environment), (newTokenColor) => ({ place: outFlow.target, token: new Token(newTokenColor) })); }); // combine them const tokenWrites = utils.crossProduct(tokenWritesPerFlow); return _.map(tokenWrites, (tokenWrite) => { // Now, we have all data we need: We have selected a transition, token read and token write. // The final step is to assemble the corresponding Move object let newTokenMap: TokenMap | null = null; const consumedTokens: TokenMap = {}; _.each(tokenRead.consumedTokens, ({ place, token }) => { if (!consumedTokens[place.key]) consumedTokens[place.key] = []; consumedTokens[place.key].push(token); }); if (!options!.ignoreInFlowTokenCounts) { newTokenMap = marking.cloneTokens(); pullConsumedTokens(newTokenMap, consumedTokens); } const producedTokens: TokenMap = {}; _.each(tokenWrite, ({ place, token }) => { if (!producedTokens[place.key]) producedTokens[place.key] = []; producedTokens[place.key].push(token); if (!options!.ignoreInFlowTokenCounts) { // ! is OK – we know newTokenMap was initialized in this case if (!newTokenMap![place.key]) newTokenMap![place.key] = []; newTokenMap![place.key].push(token); } }); let newMarking = null; if (!options!.ignoreInFlowTokenCounts) { // ! is OK – we know newTokenMap was initialized in this case newMarking = new Marking(marking.net, newTokenMap!, marking.extensions, marking); } return new Move(marking.net, newMarking, this, consumedTokens, environment, producedTokens); }); }); } } type PetriNetExtensions = { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; type PetriNetObject = { readonly places: PlaceObject[]; readonly transitions: TransitionObject[]; readonly initialMarking: MarkingObject; readonly extensions: PetriNetExtensions; } class PetriNet { // we override the not-null assertions because PetriNets may only be constructed through fromObject, which initializes these fields private _places!: { [placeKey: string]: Place }; private _transitions!: { [transitionKey: string]: Transition }; private _initialMarking!: Marking; private _extensions!: PetriNetExtensions; public get places(): { [placeKey: string]: Place } { return this._places; } public get transitions(): { [transitionKey: string]: Transition } { return this._transitions; } public get initialMarking(): Marking { return this._initialMarking; } public get extensions(): PetriNetExtensions { return this._extensions; } static fromObject(data: PetriNetObject): PetriNet { const net = new PetriNet(); net._places = _.reduce(data.places, (map: { [placeKey: string]: Place }, placeData) => { map[placeData.key] = Place.fromObject(net, placeData); return map; }, {}); net._transitions = _.reduce(data.transitions, (map: { [placeKey: string]: Transition }, transitionData) => { map[transitionData.key] = Transition.fromObject(net, transitionData); return map; }, {}); net._initialMarking = Marking.fromObject(net, data.initialMarking); net._extensions = data.extensions; return net; } toObject(): PetriNetObject { return { places: _.map(this.places, (place) => place.toObject()), transitions: _.map(this.transitions, (transition) => transition.toObject()), initialMarking: this.initialMarking.toObject(), extensions: this.extensions }; } } export { Place, PlaceObject, Transition, TransitionObject, InFlow, OutFlow, InFlowIntermediate, OutFlowIntermediate, InFlowObject, OutFlowObject, TransitionExtensions, Token, TokenObject, Move, MoveObject, Marking, MarkingObject, MarkingExtensions, PetriNet, PetriNetObject };