dsh-model-everything 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.en-US.md +332 -0
- package/README.md +332 -3
- package/cordis.patch.yml +20 -0
- package/examples/README.md +48 -0
- package/examples/control-dispatcher.json +113 -0
- package/examples/control-dispatcher.overbudget.json +113 -0
- package/examples/ecommerce-data-model.json +185 -0
- package/examples/order-state-machine.after.json +116 -0
- package/examples/order-state-machine.before.json +101 -0
- package/examples/user-migration.after.json +29 -0
- package/examples/user-migration.before.json +29 -0
- package/gate/locale/en.json +6 -0
- package/gate/locale/zh.json +6 -0
- package/gate/package.json +5 -0
- package/lib/baseline.js +249 -0
- package/lib/compose-tool.js +80 -0
- package/lib/concurrency-tool.js +40 -0
- package/lib/concurrency.js +206 -0
- package/lib/data-engine.js +1141 -0
- package/lib/data-tool.js +87 -0
- package/lib/design-engine.js +1149 -0
- package/lib/design-tool.js +63 -0
- package/lib/engine.js +4343 -0
- package/lib/export-tool.js +78 -0
- package/lib/exporters.js +434 -0
- package/lib/gate.js +274 -0
- package/lib/granularity.js +237 -0
- package/lib/index.js +237 -0
- package/lib/json-value.js +18 -0
- package/lib/model-file.js +48 -0
- package/lib/option-shape.js +62 -0
- package/lib/report-diff-tool.js +63 -0
- package/lib/report-view.js +433 -0
- package/lib/structure-tool.js +91 -0
- package/lib/structure.js +711 -0
- package/lib/tool.js +94 -0
- package/lib/types/baseline.d.ts +65 -0
- package/lib/types/compose-tool.d.ts +7 -0
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +37 -0
- package/lib/types/data-engine.d.ts +211 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/design-engine.d.ts +215 -0
- package/lib/types/design-tool.d.ts +11 -0
- package/lib/types/engine.d.ts +494 -0
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +37 -0
- package/lib/types/gate.d.ts +91 -0
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +45 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/model-file.d.ts +7 -0
- package/lib/types/option-shape.d.ts +22 -0
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/report-view.d.ts +50 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +152 -0
- package/lib/types/tool.d.ts +11 -0
- package/lib/types/uml-communication.d.ts +109 -0
- package/lib/types/uml-composite.d.ts +107 -0
- package/lib/types/uml-interaction-overview.d.ts +91 -0
- package/lib/types/uml-profile.d.ts +112 -0
- package/lib/types/uml-structure.d.ts +53 -0
- package/lib/types/uml-syntax.d.ts +296 -0
- package/lib/types/uml-timing.d.ts +152 -0
- package/lib/types/uml-tool.d.ts +12 -0
- package/lib/types/uml-usecase.d.ts +58 -0
- package/lib/types/uml.d.ts +303 -0
- package/lib/uml-communication.js +353 -0
- package/lib/uml-composite.js +442 -0
- package/lib/uml-interaction-overview.js +277 -0
- package/lib/uml-profile.js +373 -0
- package/lib/uml-structure.js +306 -0
- package/lib/uml-syntax.js +545 -0
- package/lib/uml-timing.js +974 -0
- package/lib/uml-tool.js +213 -0
- package/lib/uml-usecase.js +532 -0
- package/lib/uml.js +2110 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -6
- package/skills/model-everything/SKILL.md +450 -0
- package/skills/model-everything/references/dsh-model-schema.md +437 -0
- package/skills/model-everything/references/gap-routing-guide.md +42 -0
- package/skills/model-everything/references/hash-spec.md +152 -0
- package/skills/model-everything/references/logic-verification-guide.md +422 -0
- package/skills/model-everything-datamodel/SKILL.md +130 -0
- package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
- package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
- package/skills/model-everything-design/SKILL.md +115 -0
- package/skills/model-everything-interleaving/SKILL.md +63 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
- package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
- package/skills/model-everything-report-reading/SKILL.md +137 -0
- package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
- package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
- package/skills/model-everything-structure/SKILL.md +96 -0
- package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
- package/skills/model-everything-uml/SKILL.md +60 -0
- package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
- package/src/baseline.ts +288 -0
- package/src/compose-tool.ts +82 -0
- package/src/concurrency-tool.ts +44 -0
- package/src/concurrency.ts +254 -0
- package/src/data-engine.ts +1208 -0
- package/src/data-tool.ts +91 -0
- package/src/design-engine.ts +1263 -0
- package/src/design-tool.ts +68 -0
- package/src/engine.ts +4564 -0
- package/src/export-tool.ts +81 -0
- package/src/exporters.ts +399 -0
- package/src/gate.ts +330 -0
- package/src/granularity.ts +312 -0
- package/src/index.ts +289 -0
- package/src/json-value.ts +20 -0
- package/src/model-file.ts +49 -0
- package/src/option-shape.ts +64 -0
- package/src/report-diff-tool.ts +67 -0
- package/src/report-view.ts +483 -0
- package/src/structure-tool.ts +93 -0
- package/src/structure.ts +786 -0
- package/src/tool.ts +103 -0
- package/src/uml-communication.ts +428 -0
- package/src/uml-composite.ts +520 -0
- package/src/uml-interaction-overview.ts +321 -0
- package/src/uml-profile.ts +439 -0
- package/src/uml-structure.ts +317 -0
- package/src/uml-syntax.ts +764 -0
- package/src/uml-timing.ts +1106 -0
- package/src/uml-tool.ts +230 -0
- package/src/uml-usecase.ts +566 -0
- package/src/uml.ts +2139 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, validateModel, verdictOf } from './engine.js';
|
|
3
|
+
import { ExportError, exportModel } from './exporters.js';
|
|
4
|
+
import { renderReportText } from './report-view.js';
|
|
5
|
+
/** The model hash of an export's input, when the input validates (the success path always does). */
|
|
6
|
+
function exportHashes(model) {
|
|
7
|
+
const validation = validateModel(model);
|
|
8
|
+
return validation.ok
|
|
9
|
+
? { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(validation.model) }
|
|
10
|
+
: { hashSpec: DEFAULT_HASH_SPEC };
|
|
11
|
+
}
|
|
12
|
+
export const EXPORT_TOOL_NAME = 'model_export';
|
|
13
|
+
/**
|
|
14
|
+
* DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
|
|
15
|
+
* native input for the tool this plugin routes to (UPPAAL / TLA+ / PRISM / SPIN).
|
|
16
|
+
* v1 translates the core machine; unrepresentable invariants become warnings.
|
|
17
|
+
*/
|
|
18
|
+
export const modelExportTool = defineTool({
|
|
19
|
+
name: EXPORT_TOOL_NAME,
|
|
20
|
+
description: 'Translate a LogicModelV1 into the input language of an external checker - UPPAAL, TLA+, PRISM or SPIN - for a property this plugin does not decide itself (symbolic model checking, unbounded induction, expected reward). format is one of uppaal (.xta + queries), tla (TLC spec + safety property), prism (.pm + .pctl), spin (Promela + ltl). This tool does not verify anything: for the checks it can decide use `model_verify`. Booleans are exported as 0/1 integers; invariants the target cannot express are returned as warnings, never silently dropped. Returns { ok, format, primary, extras?, warnings? } or { ok: false, error } when the model is invalid or the PRISM enumeration is too large.',
|
|
21
|
+
parameters: {
|
|
22
|
+
model: {
|
|
23
|
+
type: 'json',
|
|
24
|
+
required: true,
|
|
25
|
+
description: 'LogicModelV1 state-machine model to export.',
|
|
26
|
+
},
|
|
27
|
+
format: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
required: true,
|
|
30
|
+
enum: ['uppaal', 'tla', 'prism', 'spin'],
|
|
31
|
+
description: 'Target tool format.',
|
|
32
|
+
},
|
|
33
|
+
reportView: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['digest', 'full'],
|
|
36
|
+
description: 'digest (default) prints the generated file verbatim; full prints the structured result as JSON.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
output: {
|
|
40
|
+
schema: {
|
|
41
|
+
type: 'json',
|
|
42
|
+
description: 'Export result with generated file content.',
|
|
43
|
+
},
|
|
44
|
+
render(args, value) {
|
|
45
|
+
return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
timeoutMs: 10_000,
|
|
49
|
+
isConcurrencySafe: () => true,
|
|
50
|
+
async execute(args) {
|
|
51
|
+
try {
|
|
52
|
+
const result = exportModel(args.model, args.format);
|
|
53
|
+
const warnings = result.warnings ?? [];
|
|
54
|
+
const nextSteps = [];
|
|
55
|
+
if (warnings.length > 0)
|
|
56
|
+
nextSteps.push('The target notation cannot express some invariants; the warnings name each one — verify them in the target tool rather than assuming they carried over.');
|
|
57
|
+
nextSteps.push('Run the exported file through its own tool (verify/compile) before trusting the translation; this plugin guarantees the encoding, not the target checker\'s verdict.');
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
ran: true,
|
|
61
|
+
...verdictOf(0, warnings.length),
|
|
62
|
+
schema: REPORT_SCHEMAS.export,
|
|
63
|
+
format: result.format,
|
|
64
|
+
primary: result.primary,
|
|
65
|
+
...(result.extras === undefined ? {} : { extras: result.extras }),
|
|
66
|
+
hashes: exportHashes(args.model),
|
|
67
|
+
warnings,
|
|
68
|
+
nextSteps,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
// The exporter's own refusals carry their code: a limit of the encoding is not a defect in the model.
|
|
74
|
+
const errorCode = error instanceof ExportError ? error.code : 'MODEL_INVALID';
|
|
75
|
+
return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.export, errorCode, error: message };
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
package/lib/exporters.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// External-tool exporters: turn a validated LogicModelV1 into native input for the
|
|
2
|
+
// tools this plugin routes to (see skills/model-everything/references/gap-routing-guide.md).
|
|
3
|
+
// v1 translates the CORE machine (states, events, transitions, integer/boolean guards
|
|
4
|
+
// and updates). Constructs a target cannot express are emitted as warnings/comments,
|
|
5
|
+
// never silently dropped. Booleans are exported as 0/1 integers everywhere.
|
|
6
|
+
import { validateModel } from './engine.js';
|
|
7
|
+
export function prepareModel(input) {
|
|
8
|
+
const validation = validateModel(input);
|
|
9
|
+
if (!validation.ok)
|
|
10
|
+
throw new Error('model invalid: ' + validation.errors.join('; '));
|
|
11
|
+
const model = validation.model;
|
|
12
|
+
const used = new Set();
|
|
13
|
+
const stateIds = [];
|
|
14
|
+
const indexOf = new Map();
|
|
15
|
+
const stateIdOf = new Map();
|
|
16
|
+
const sanitize = (raw) => {
|
|
17
|
+
let out = raw.replace(/[^A-Za-z0-9_]/g, '_');
|
|
18
|
+
if (out.length === 0 || /^[0-9]/.test(out))
|
|
19
|
+
out = 's_' + out;
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
model.states.forEach((state, index) => {
|
|
23
|
+
let candidate = sanitize(state.id);
|
|
24
|
+
if (used.has(candidate))
|
|
25
|
+
candidate = candidate + '_' + index;
|
|
26
|
+
used.add(candidate);
|
|
27
|
+
stateIds.push(candidate);
|
|
28
|
+
indexOf.set(state.id, index);
|
|
29
|
+
stateIdOf.set(state.id, candidate);
|
|
30
|
+
});
|
|
31
|
+
return { model, stateIds, indexOf, initId: stateIdOf.get(model.init) ?? stateIds[0], stateIdOf };
|
|
32
|
+
}
|
|
33
|
+
function numValue(value) {
|
|
34
|
+
return typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
35
|
+
}
|
|
36
|
+
function leafExpr(node, varName) {
|
|
37
|
+
return varName(node.variable) + ' ' + node.op + ' ' + String(numValue(node.value));
|
|
38
|
+
}
|
|
39
|
+
function guardExpr(node, and, or, not, varName, opFor = (op) => op) {
|
|
40
|
+
if (node === undefined)
|
|
41
|
+
return '';
|
|
42
|
+
if ('variable' in node)
|
|
43
|
+
return varName(node.variable) + ' ' + opFor(node.op) + ' ' + String(numValue(node.value));
|
|
44
|
+
if ('all' in node)
|
|
45
|
+
return '(' + node.all.map((g) => guardExpr(g, and, or, not, varName, opFor)).join(' ' + and + ' ') + ')';
|
|
46
|
+
if ('any' in node)
|
|
47
|
+
return '(' + node.any.map((g) => guardExpr(g, and, or, not, varName, opFor)).join(' ' + or + ' ') + ')';
|
|
48
|
+
if ('not' in node)
|
|
49
|
+
return not + '(' + guardExpr(node.not, and, or, not, varName, opFor) + ')';
|
|
50
|
+
return 'true';
|
|
51
|
+
}
|
|
52
|
+
function updateAssignments(updates, varName) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const update of updates ?? []) {
|
|
55
|
+
const name = varName(update.variable);
|
|
56
|
+
const value = update.value ?? 1;
|
|
57
|
+
if (update.op === 'set')
|
|
58
|
+
out.push(name + ' := ' + String(value));
|
|
59
|
+
else if (update.op === 'inc')
|
|
60
|
+
out.push(name + ' := ' + name + ' + ' + String(value));
|
|
61
|
+
else
|
|
62
|
+
out.push(name + ' := ' + name + ' - ' + String(value));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function forbiddenIndexes(ex) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const invariant of ex.model.invariants ?? []) {
|
|
69
|
+
if (invariant.kind === 'never-states') {
|
|
70
|
+
for (const s of invariant.states) {
|
|
71
|
+
const index = ex.indexOf.get(s);
|
|
72
|
+
if (index !== undefined)
|
|
73
|
+
out.push(index);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------- UPPAAL ----
|
|
80
|
+
// UPPAAL .xta is XML (flat-1_6 DTD): nta/template/location/transition with label
|
|
81
|
+
// kinds guard | assignment | comments. Queries are plain text (one per line).
|
|
82
|
+
function exportUppaal(ex) {
|
|
83
|
+
const { model, stateIds, indexOf, initId } = ex;
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
86
|
+
const safeEvent = (event) => xml(event.replace(/[^A-Za-z0-9_]/g, '_'));
|
|
87
|
+
const globals = (model.variables ?? []).map((v) => 'int ' + v.name + ' = ' + String(numValue(v.init)) + ';').join('\n');
|
|
88
|
+
const locs = model.states.map((state, i) => {
|
|
89
|
+
const nameEl = '\n <name x=\"16\" y=\"16\">' + xml(stateIdOf(ex, state.id)) + '</name>';
|
|
90
|
+
return ' <location id=\"id' + i + '\" x=\"' + (i * 120) + '\" y=\"0\">' + nameEl + '\n </location>';
|
|
91
|
+
}).join('\n');
|
|
92
|
+
const edges = model.transitions.map((tr, i) => {
|
|
93
|
+
const guard = guardExpr(tr.guard, '&&', '||', '!', (v) => v);
|
|
94
|
+
const assigns = updateAssignments(tr.updates, (v) => v);
|
|
95
|
+
const labels = [];
|
|
96
|
+
if (guard !== '')
|
|
97
|
+
labels.push(' <label kind=\"guard\" x=\"16\" y=\"16\">' + xml(guard) + '</label>');
|
|
98
|
+
if (assigns.length > 0)
|
|
99
|
+
labels.push(' <label kind=\"assignment\" x=\"16\" y=\"16\">' + xml(assigns.join(',\n')) + '</label>');
|
|
100
|
+
// The event name is a comment, not a synchronisation. A single machine exports as one template: `e!` waits for a
|
|
101
|
+
// partner this file does not contain, so every transition would block forever - and the channel was never
|
|
102
|
+
// declared either, so UPPAAL would not load the file. The event is documentation here; rendezvous between
|
|
103
|
+
// machines is a composition question, and `model_compose_verify` is where that is decided.
|
|
104
|
+
labels.push(' <label kind=\"comments\" x=\"16\" y=\"16\">' + xml(tr.event) + '</label>');
|
|
105
|
+
return ' <transition id=\"id' + String(1000 + i) + '\">\n <source ref=\"id' + String(indexOf.get(tr.from)) + '\"/>\n <target ref=\"id' + String(indexOf.get(tr.to)) + '\"/>\n' + labels.join('\n') + '\n </transition>';
|
|
106
|
+
}).join('\n');
|
|
107
|
+
const queries = [];
|
|
108
|
+
for (const state of model.states)
|
|
109
|
+
if (state.terminal)
|
|
110
|
+
queries.push('E<> ModelEverything.' + stateIdOf(ex, state.id));
|
|
111
|
+
for (const index of forbiddenIndexes(ex))
|
|
112
|
+
queries.push('A[] not ModelEverything.' + stateIds[index]);
|
|
113
|
+
for (const invariant of model.invariants ?? []) {
|
|
114
|
+
if (invariant.kind !== 'never-states')
|
|
115
|
+
warnings.push('UPPAAL export carries invariant kind ' + invariant.kind + ' only as a comment (not expressible in the exported XML).');
|
|
116
|
+
}
|
|
117
|
+
const initIndex = indexOf.get(model.init) ?? 0;
|
|
118
|
+
const xta = '<?xml version="1.0" encoding="utf-8"?>\n' +
|
|
119
|
+
'<!DOCTYPE nta PUBLIC \'-//Uppaal Team//DTD Flat System 1.6//EN\' \'http://www.it.uu.se/research/group/darts/uppaal/flat-1_6.dtd\'>\n' +
|
|
120
|
+
'<nta>\n' +
|
|
121
|
+
' <declaration>// Generated by this plugin (UPPAAL XML export; booleans as int).\n' + (globals === '' ? '' : globals + '\n') + '</declaration>\n' +
|
|
122
|
+
' <template>\n' +
|
|
123
|
+
' <name x=\"5\" y=\"5\">ModelEverything</name>\n' +
|
|
124
|
+
' <declaration/>\n' +
|
|
125
|
+
locs + '\n' +
|
|
126
|
+
' <init ref=\"id' + String(initIndex) + '\"/>\n' +
|
|
127
|
+
edges + '\n' +
|
|
128
|
+
' </template>\n' +
|
|
129
|
+
' <system>system ModelEverything;</system>\n' +
|
|
130
|
+
'</nta>\n';
|
|
131
|
+
const extras = { queries: queries.join('\n') + (queries.length ? '\n' : '') };
|
|
132
|
+
void initId;
|
|
133
|
+
return { format: 'uppaal', primary: xta, extras, warnings };
|
|
134
|
+
}
|
|
135
|
+
function stateIdOf(ex, id) {
|
|
136
|
+
return ex.stateIdOf.get(id) ?? id;
|
|
137
|
+
}
|
|
138
|
+
// ------------------------------------------------------------------ TLA+ ----
|
|
139
|
+
// ------------------------------------------------------------------ TLA+ ----
|
|
140
|
+
function exportTla(ex) {
|
|
141
|
+
const { model, stateIds, stateIdOf, initId } = ex;
|
|
142
|
+
const warnings = [];
|
|
143
|
+
const bs = String.fromCharCode(92); // backslash, composed so no escape pitfalls
|
|
144
|
+
const CONJ = '/' + bs; // /\ (TLA conjunction)
|
|
145
|
+
const DISJ = bs + '/'; // \/ (TLA disjunction)
|
|
146
|
+
const MEM = bs + 'in'; // \in membership
|
|
147
|
+
const NOTIN = bs + 'notin'; // \notin
|
|
148
|
+
const variables = ['pc'].concat((model.variables ?? []).map((v) => v.name));
|
|
149
|
+
const typeVar = (model.variables ?? []).map((v) => v.name + ' ' + MEM + ' ' + String(numValue(v.min ?? 0)) + '..' + String(Math.max(1, numValue(v.max ?? 1)))).join(' ' + CONJ + ' ');
|
|
150
|
+
const initParts = ['pc = "' + initId + '"'].concat((model.variables ?? []).map((v) => v.name + ' = ' + String(numValue(v.init)))).join(' ' + CONJ + ' ');
|
|
151
|
+
const nextParts = model.transitions.map((tr) => {
|
|
152
|
+
const guard = guardExpr(tr.guard, CONJ, DISJ, '~', (v) => v).replace(/ == /g, ' = ').replace(/ != /g, ' /= ');
|
|
153
|
+
const updates = [];
|
|
154
|
+
for (const u of tr.updates ?? []) {
|
|
155
|
+
const value = u.value ?? 1;
|
|
156
|
+
if (u.op === 'set')
|
|
157
|
+
updates.push(u.variable + ' = ' + String(value));
|
|
158
|
+
else if (u.op === 'inc')
|
|
159
|
+
updates.push(u.variable + ' = ' + u.variable + ' + ' + String(value));
|
|
160
|
+
else
|
|
161
|
+
updates.push(u.variable + ' = ' + u.variable + ' - ' + String(value));
|
|
162
|
+
}
|
|
163
|
+
const pcPart = 'pc' + "'" + ' = "' + stateIdOf.get(tr.to) + '"';
|
|
164
|
+
const rest = updates.length ? ' ' + CONJ + ' ' + updates.join(' ' + CONJ + ' ') : '';
|
|
165
|
+
const guardPart = guard === '' ? 'pc = "' + stateIdOf.get(tr.from) + '"' : 'pc = "' + stateIdOf.get(tr.from) + '" ' + CONJ + ' ' + guard;
|
|
166
|
+
return DISJ + ' ' + guardPart + ' ' + CONJ + ' ' + pcPart + rest;
|
|
167
|
+
}).join('\n');
|
|
168
|
+
const forbids = forbiddenIndexes(ex).map((i) => '"' + stateIds[i] + '"');
|
|
169
|
+
const props = [];
|
|
170
|
+
if (forbids.length)
|
|
171
|
+
props.push('CheckSafety == [] (pc ' + NOTIN + ' {' + forbids.join(', ') + '})');
|
|
172
|
+
for (const invariant of model.invariants ?? []) {
|
|
173
|
+
if (invariant.kind !== 'never-states')
|
|
174
|
+
warnings.push('TLA+ v1 does not translate invariant kind ' + invariant.kind + '; only never-states becomes a property.');
|
|
175
|
+
}
|
|
176
|
+
let spec = '---- MODULE ModelEverything ----\n' +
|
|
177
|
+
'EXTENDS Integers\n' +
|
|
178
|
+
'VARIABLES ' + variables.join(', ') + '\n' +
|
|
179
|
+
'States == {' + stateIds.map((s) => '"' + s + '"').join(', ') + '}\n' +
|
|
180
|
+
'TypeOK == pc ' + MEM + ' States' + (typeVar === '' ? '' : ' ' + CONJ + ' ' + typeVar) + '\n' +
|
|
181
|
+
'Init == ' + initParts + '\n' +
|
|
182
|
+
'Next ==\n' + nextParts + '\n' +
|
|
183
|
+
(props.length ? '\n' + props.join('\n\n') + '\n' : '') +
|
|
184
|
+
'====\n';
|
|
185
|
+
const extras = props.length ? { properties: props.join('\n') + '\n' } : {};
|
|
186
|
+
return { format: 'tla', primary: spec, extras, warnings };
|
|
187
|
+
}
|
|
188
|
+
// ----------------------------------------------------------------- PRISM ----
|
|
189
|
+
function exportPrism(ex) {
|
|
190
|
+
const { model, indexOf, stateIdOf, stateIds } = ex;
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const ranges = (model.variables ?? []).map((v) => ({
|
|
193
|
+
name: v.name,
|
|
194
|
+
lo: numValue(v.min ?? 0),
|
|
195
|
+
hi: Math.max(1, numValue(v.max ?? 3)),
|
|
196
|
+
init: numValue(v.init),
|
|
197
|
+
}));
|
|
198
|
+
const valuations = [];
|
|
199
|
+
const build = (i, acc) => {
|
|
200
|
+
if (i === ranges.length) {
|
|
201
|
+
valuations.push(acc);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (let v = ranges[i].lo; v <= ranges[i].hi; v++) {
|
|
205
|
+
const copy = { ...acc };
|
|
206
|
+
copy[ranges[i].name] = v;
|
|
207
|
+
build(i + 1, copy);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
build(0, {});
|
|
211
|
+
const size = model.states.length * Math.max(1, valuations.length);
|
|
212
|
+
if (size > 20000) {
|
|
213
|
+
throw new ExportError('EXPORT_TOO_LARGE', 'the PRISM encoding enumerates states x variable valuations and this model needs ' + String(model.states.length) + ' x ' + String(valuations.length) + ' = ' + String(size) + ' of them, over the 20000 this exporter writes. The model is valid; the flat enumeration is what does not fit. Narrow the variable domains, or verify the model with `model_verify` and export only the machine for the properties that need PRISM.');
|
|
214
|
+
}
|
|
215
|
+
const evalGuard = (guard, val) => {
|
|
216
|
+
if (guard === undefined)
|
|
217
|
+
return true;
|
|
218
|
+
if ('variable' in guard) {
|
|
219
|
+
const left = val[guard.variable] ?? numValue((model.variables ?? []).find((v) => v.name === guard.variable)?.init ?? 0);
|
|
220
|
+
const right = numValue(guard.value);
|
|
221
|
+
switch (guard.op) {
|
|
222
|
+
case '==': return left === right;
|
|
223
|
+
case '!=': return left !== right;
|
|
224
|
+
case '<': return left < right;
|
|
225
|
+
case '<=': return left <= right;
|
|
226
|
+
case '>': return left > right;
|
|
227
|
+
case '>=': return left >= right;
|
|
228
|
+
default: return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if ('all' in guard)
|
|
232
|
+
return guard.all.every((g) => evalGuard(g, val));
|
|
233
|
+
if ('any' in guard)
|
|
234
|
+
return guard.any.some((g) => evalGuard(g, val));
|
|
235
|
+
if ('not' in guard)
|
|
236
|
+
return !evalGuard(guard.not, val);
|
|
237
|
+
return false;
|
|
238
|
+
};
|
|
239
|
+
const applyUpdates = (t, val) => {
|
|
240
|
+
const out = { ...val };
|
|
241
|
+
for (const u of t.updates ?? []) {
|
|
242
|
+
const cur = out[u.variable] ?? 0;
|
|
243
|
+
const value = u.value ?? 1;
|
|
244
|
+
if (u.op === 'set')
|
|
245
|
+
out[u.variable] = value;
|
|
246
|
+
else if (u.op === 'inc')
|
|
247
|
+
out[u.variable] = cur + value;
|
|
248
|
+
else
|
|
249
|
+
out[u.variable] = cur - value;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
};
|
|
253
|
+
const commands = [];
|
|
254
|
+
for (const state of model.states) {
|
|
255
|
+
for (const val of valuations) {
|
|
256
|
+
const groups = new Map();
|
|
257
|
+
for (const t of model.transitions)
|
|
258
|
+
if (t.from === state.id) {
|
|
259
|
+
const list = groups.get(t.event) ?? [];
|
|
260
|
+
list.push(t);
|
|
261
|
+
groups.set(t.event, list);
|
|
262
|
+
}
|
|
263
|
+
const outcomes = new Map();
|
|
264
|
+
let totalW = 0;
|
|
265
|
+
for (const group of groups.values()) {
|
|
266
|
+
const guarded = group.filter((t) => t.guard !== undefined && evalGuard(t.guard, val));
|
|
267
|
+
const chosen = guarded.length > 0 ? guarded : group.filter((t) => t.guard === undefined);
|
|
268
|
+
for (const t of chosen) {
|
|
269
|
+
const weight = t.weight ?? 1;
|
|
270
|
+
const nv = applyUpdates(t, val);
|
|
271
|
+
const key = String(indexOf.get(t.to)) + '|' + JSON.stringify(nv);
|
|
272
|
+
const cur = outcomes.get(key);
|
|
273
|
+
if (cur === undefined) {
|
|
274
|
+
outcomes.set(key, { weight, pc: indexOf.get(t.to) ?? 0, val: nv });
|
|
275
|
+
totalW += weight;
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
cur.weight += weight;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (outcomes.size === 0)
|
|
282
|
+
continue;
|
|
283
|
+
const guardParts = ['pc = ' + String(indexOf.get(state.id) ?? 0)];
|
|
284
|
+
for (const r of ranges)
|
|
285
|
+
guardParts.push(r.name + ' = ' + String(val[r.name] ?? r.init));
|
|
286
|
+
const terms = [];
|
|
287
|
+
for (const item of outcomes.values()) {
|
|
288
|
+
const p = totalW > 0 ? item.weight / totalW : 0;
|
|
289
|
+
const prob = p.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
|
|
290
|
+
const upd = ["(pc' = " + String(item.pc) + ")"];
|
|
291
|
+
for (const r of ranges) {
|
|
292
|
+
const v = item.val[r.name] ?? r.init;
|
|
293
|
+
if (v !== (val[r.name] ?? r.init))
|
|
294
|
+
upd.push("(" + r.name + "' = " + String(v) + ")");
|
|
295
|
+
}
|
|
296
|
+
terms.push(prob + ' : ' + upd.join(' & '));
|
|
297
|
+
}
|
|
298
|
+
commands.push('[] ' + guardParts.join(' & ') + ' ->\n ' + terms.join(' +\n '));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let module = '// Generated by this plugin (DTMC export; booleans as 0/1 integers).\n' +
|
|
302
|
+
'dtmc\n\n' +
|
|
303
|
+
'module ModelEverything\n' +
|
|
304
|
+
' pc : [0..' + String(model.states.length - 1) + '] init ' + String(indexOf.get(model.init) ?? 0) + ';\n' +
|
|
305
|
+
(ranges.map((r) => ' ' + r.name + ' : [' + String(r.lo) + '..' + String(r.hi) + '] init ' + String(r.init) + ';').join('\n')) + '\n\n' +
|
|
306
|
+
commands.join('\n\n') + '\n' +
|
|
307
|
+
'endmodule\n';
|
|
308
|
+
const labels = [];
|
|
309
|
+
for (const state of model.states)
|
|
310
|
+
if (state.terminal)
|
|
311
|
+
labels.push('label "term_' + (stateIdOf.get(state.id) ?? 's') + '" = pc = ' + String(indexOf.get(state.id)));
|
|
312
|
+
const forb = forbiddenIndexes(ex);
|
|
313
|
+
if (forb.length)
|
|
314
|
+
labels.push('label "forbidden" = ' + forb.map((i) => 'pc = ' + String(i)).join(' | '));
|
|
315
|
+
const pctl = [];
|
|
316
|
+
for (const invariant of model.invariants ?? []) {
|
|
317
|
+
if (invariant.kind === 'probability') {
|
|
318
|
+
const target = indexOf.get(invariant.target);
|
|
319
|
+
if (target !== undefined) {
|
|
320
|
+
labels.push('label "target" = pc = ' + String(target));
|
|
321
|
+
pctl.push('P' + invariant.op + String(invariant.p) + ' [ F "target" ]');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (forb.length)
|
|
326
|
+
pctl.push('P>=1 [ G !"forbidden" ]');
|
|
327
|
+
module = module + '\n' + labels.join('\n') + '\n';
|
|
328
|
+
const extras = { properties: pctl.join('\n') + '\n' };
|
|
329
|
+
for (const invariant of model.invariants ?? []) {
|
|
330
|
+
if (invariant.kind !== 'probability' && invariant.kind !== 'never-states') {
|
|
331
|
+
warnings.push('PRISM v1 does not translate invariant kind ' + invariant.kind + '.');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
void stateIds;
|
|
335
|
+
return { format: 'prism', primary: module, extras, warnings };
|
|
336
|
+
}
|
|
337
|
+
// ------------------------------------------------------------------ SPIN ----
|
|
338
|
+
function exportSpin(ex) {
|
|
339
|
+
const { model, indexOf } = ex;
|
|
340
|
+
const warnings = [];
|
|
341
|
+
const varDecl = (model.variables ?? []).map((v) => 'int ' + v.name + ' = ' + String(numValue(v.init)) + ';').join('\n');
|
|
342
|
+
const lines = [];
|
|
343
|
+
const promAssigns = (updates) => {
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const u of updates ?? []) {
|
|
346
|
+
const value = u.value ?? 1;
|
|
347
|
+
if (u.op === 'set')
|
|
348
|
+
out.push(u.variable + ' = ' + String(value));
|
|
349
|
+
else if (u.op === 'inc')
|
|
350
|
+
out.push(u.variable + ' = ' + u.variable + ' + ' + String(value));
|
|
351
|
+
else
|
|
352
|
+
out.push(u.variable + ' = ' + u.variable + ' - ' + String(value));
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
};
|
|
356
|
+
for (const t of model.transitions) {
|
|
357
|
+
const guard = guardExpr(t.guard, '&&', '||', '!', (v) => v);
|
|
358
|
+
const assigns = promAssigns(t.updates);
|
|
359
|
+
const cond = guard === '' ? 'pc == ' + String(indexOf.get(t.from)) : 'pc == ' + String(indexOf.get(t.from)) + ' && ' + guard;
|
|
360
|
+
const prefix = assigns.length ? ' ' + assigns.join('; ') + ';' : '';
|
|
361
|
+
lines.push(' :: (' + cond + ') ->' + prefix + ' pc = ' + String(indexOf.get(t.to)) + ';');
|
|
362
|
+
}
|
|
363
|
+
for (const state of model.states)
|
|
364
|
+
if (state.terminal)
|
|
365
|
+
lines.push(' :: (pc == ' + String(indexOf.get(state.id)) + ') -> goto done;');
|
|
366
|
+
const promela = '// Generated by this plugin (Promela v1; booleans as 0/1 integers).\n' +
|
|
367
|
+
'int pc = ' + String(indexOf.get(model.init) ?? 0) + ';\n' +
|
|
368
|
+
(varDecl === '' ? '' : varDecl + '\n') +
|
|
369
|
+
'active proctype ModelEverything() {\n' +
|
|
370
|
+
' do\n' +
|
|
371
|
+
lines.join('\n') + '\n' +
|
|
372
|
+
' od\n' +
|
|
373
|
+
'done: skip\n' +
|
|
374
|
+
'}\n';
|
|
375
|
+
const extras = {};
|
|
376
|
+
const forb = forbiddenIndexes(ex);
|
|
377
|
+
if (forb.length)
|
|
378
|
+
extras.properties = 'ltl safety { [] (!(pc == ' + forb.map((i) => String(i)).join(') && !(pc == ') + ')) }\n';
|
|
379
|
+
for (const invariant of model.invariants ?? []) {
|
|
380
|
+
if (invariant.kind !== 'never-states')
|
|
381
|
+
warnings.push('SPIN v1 does not translate invariant kind ' + invariant.kind + '.');
|
|
382
|
+
}
|
|
383
|
+
return { format: 'spin', primary: promela, extras, warnings };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Every format this exporter can produce, in one place.
|
|
387
|
+
*
|
|
388
|
+
* The absence of this list is what made an unknown format dangerous: the function ended with
|
|
389
|
+
* `return exportSpin(ex)`, so `'mermaid'`, `42`, `''` and `undefined` all produced Promela and
|
|
390
|
+
* reported `format: 'spin'`. A caller who named a notation this exporter cannot write received a
|
|
391
|
+
* different notation's output and a report naming a format they had not asked for.
|
|
392
|
+
*/
|
|
393
|
+
export const EXPORT_FORMATS = ['uppaal', 'tla', 'prism', 'spin'];
|
|
394
|
+
export function isExportFormat(value) {
|
|
395
|
+
return typeof value === 'string' && EXPORT_FORMATS.includes(value);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* A refusal an exporter knows by name.
|
|
399
|
+
*
|
|
400
|
+
* The tool mapped every throw onto `MODEL_INVALID`, so "this valid model is too large for the PRISM encoding" came
|
|
401
|
+
* back as "your model is invalid" - a claim about the caller's work made by a limit of the exporter.
|
|
402
|
+
*/
|
|
403
|
+
export class ExportError extends Error {
|
|
404
|
+
code;
|
|
405
|
+
constructor(code, message) {
|
|
406
|
+
super(message);
|
|
407
|
+
this.code = code;
|
|
408
|
+
this.name = 'ExportError';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
export function exportModel(input, format) {
|
|
412
|
+
if (!isExportFormat(format)) {
|
|
413
|
+
const arrived = format === undefined ? 'undefined' : JSON.stringify(format);
|
|
414
|
+
throw new Error('format must be one of ' + EXPORT_FORMATS.join(' | ') + ', got ' + arrived + '. Each one writes a different notation, so there is no default to fall back to.');
|
|
415
|
+
}
|
|
416
|
+
const ex = prepareModel(input);
|
|
417
|
+
const result = format === 'uppaal' ? exportUppaal(ex)
|
|
418
|
+
: format === 'tla' ? exportTla(ex)
|
|
419
|
+
: format === 'prism' ? exportPrism(ex)
|
|
420
|
+
: exportSpin(ex);
|
|
421
|
+
// Fields this model declares that the target has no place for. They are reported rather than dropped: a reader who
|
|
422
|
+
// sees the export trusts it as the model, and a check that ran on the field is then missing from what the target
|
|
423
|
+
// verifies.
|
|
424
|
+
const dropped = [];
|
|
425
|
+
if (ex.model.transitions.some((transition) => transition.cost !== undefined)) {
|
|
426
|
+
dropped.push('EXPORT_FIELD_DROPPED: transition `cost` has no field in ' + format + ', so the A12 budget the model was checked against is not carried; model it as a reward/timing annotation in the target, or verify it here instead.');
|
|
427
|
+
}
|
|
428
|
+
if (ex.model.states.some((state) => state.maxTicks !== undefined)) {
|
|
429
|
+
dropped.push('EXPORT_FIELD_DROPPED: state `maxTicks` (A14, a discrete-tick deadline) has no field in ' + format + '; the target needs a clock and an invariant on the location to say the same thing.');
|
|
430
|
+
}
|
|
431
|
+
if (dropped.length === 0)
|
|
432
|
+
return result;
|
|
433
|
+
return { ...result, warnings: [...dropped, ...(result.warnings ?? [])] };
|
|
434
|
+
}
|