snappea 0.2.0

The P modeling language in Rust: emit events from your application and check them against P specs in tests, plus the `snappea` checker
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Constructors for every diagnostic message, copied from `DefaultTranslationErrorHandler`.

use crate::sema::diag::Diagnostic;
use crate::sema::program::*;
use crate::sema::types::{PType, TypeKind};
use crate::syntax::{SourceMap, Span};

/// `FormatType` upstream: the "null" type is shown as "(no value)".
pub fn format_type(t: &PType, prog: &Program) -> String {
    let rep = t.original_repr(prog);
    if rep == "null" {
        "(no value)".to_string()
    } else {
        rep
    }
}

pub struct Errors<'a> {
    pub sm: &'a SourceMap,
}

impl<'a> Errors<'a> {
    fn loc(&self, span: Span) -> String {
        self.sm.location(span).to_string()
    }

    pub fn duplicate_start_state(&self, span: Span, dup: &str, orig: &str, machine: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("state '{}' tries to replace start state '{}' in machine '{}'", dup, orig, machine),
        )
        .help("mark exactly one state of the machine `start`")
    }

    pub fn duplicate_event_action(&self, span: Span, existing: Span, state: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("event handler in state '{}' duplicates handler at {}", state, self.loc(existing)),
        )
        .help("keep one handler for this event in this state; merge their bodies or remove one")
    }

    pub fn duplicate_state_exit(&self, span: Span, existing: Span, state: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("exit handler in state '{}' duplicates exit handler at {}", state, self.loc(existing)),
        )
        .help("keep one `exit` block per state")
    }

    pub fn duplicate_state_entry(&self, span: Span, existing: Span, state: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("entry handler in state '{}' duplicates entry handler at {}", state, self.loc(existing)),
        )
        .help("keep one `entry` block per state")
    }

    pub fn duplicate_declaration(&self, span: Span, dup: &str, existing: &str, existing_span: Span) -> Diagnostic {
        Diagnostic::new(span, format!("'{}' duplicates declaration '{}' at {}", dup, existing, self.loc(existing_span)))
            .help("rename one of the two declarations")
    }

    pub fn redeclare_global_param(&self, span: Span, dup: &str, existing: &str, existing_span: Span) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "'{}' redeclares global param '{}' (originally declared at {})",
                dup,
                existing,
                self.loc(existing_span)
            ),
        )
        .help("rename this param or remove one of the declarations")
    }

    pub fn undeclared_global_param(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(span, format!("undeclared global param '{}'", name))
            .help("declare it at the top level, e.g. `param N: int;`")
    }

    pub fn modify_global_param(&self, span: Span, name: &str, decl_span: Span) -> Diagnostic {
        Diagnostic::new(span, format!("cannot modify global param '{}' (declared at {})", name, self.loc(decl_span)))
            .help("params are read-only; copy it into a local variable and change that")
    }

    pub fn invalid_twise(&self, span: Span, test_span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid twise number at {}: {}", self.loc(test_span), msg))
            .help("use a number between 1 and the number of params, e.g. `(2 wise)`")
    }

    pub fn incorrect_argument_count(&self, span: Span, actual: usize, expected: usize) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("goto, function or constructor call expected {} arguments, got {}", expected, actual),
        )
        .help("pass one argument per parameter, in declaration order")
    }

    pub fn missing_declaration(&self, span: Span, kind: &str, name: &str) -> Diagnostic {
        let d = Diagnostic::new(span, format!("could not find {} '{}'", kind, name));
        match self.similar_name(name) {
            Some(s) => d.help(format!("did you mean '{}'? Otherwise declare the {} before using it", s, kind)),
            None => d.help(format!("declare the {} before using it, or check the spelling", kind)),
        }
    }

    /// The identifier in the sources closest to `name`, if one is a likely typo of it.
    fn similar_name(&self, name: &str) -> Option<String> {
        let limit = (name.chars().count() / 3).max(1);
        let mut best: Option<(usize, String)> = None;
        for (i, f) in self.sm.files().iter().enumerate() {
            let Ok(tokens) = crate::syntax::lexer::lex(crate::syntax::FileId(i as u32), &f.text) else { continue };
            for t in tokens.iter().filter(|t| t.kind == crate::syntax::token::TokenKind::Iden) {
                let cand = f.slice(t.span);
                if cand == name {
                    continue;
                }
                let d = if cand.eq_ignore_ascii_case(name) { 0 } else { edit_distance(cand, name) };
                // a short name is "close" to every other short name; require some overlap
                let shorter = cand.chars().count().min(name.chars().count());
                if d <= limit && d < shorter && best.as_ref().is_none_or(|(bd, b)| (d, cand) < (*bd, b.as_str())) {
                    best = Some((d, cand.to_string()));
                }
            }
        }
        best.map(|(_, s)| s)
    }

    pub fn circular_typedef(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(span, format!("typedef '{}' has a circular dependency", name))
            .help("break the cycle: a type cannot contain itself")
    }

    pub fn duplicate_named_tuple_entry(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(span, format!("named tuple has duplicate field '{}'", name))
            .help("give each field a distinct name")
    }

    pub fn illegal_type_in_coerce(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "coerce expects an interface, int, or float type")
            .help("coerce to an interface, `int` or `float`, e.g. `x as int`")
    }

    pub fn illegal_interface_coerce(&self, span: Span, old: &str, new: &str, outlier: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "illegal coerce: permissions of '{}' are not a superset of '{}' (e.g., event '{}')",
                old, new, outlier
            ),
        )
        .help("coerce to an interface whose events the machine receives, or add the missing events to it")
    }

    pub fn static_function_not_allowed_as_handler(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "function '{}' cannot be used as a handler: global and foreign functions are not allowed as handlers",
                name
            ),
        )
        .help("declare the function inside the machine, or call it from a handler block: `on e do { f(); }`")
    }

    pub fn type_mismatch(&self, span: Span, actual: &PType, expected: &[&PType], prog: &Program) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "got type: {}, expected: {}",
                format_type(actual, prog),
                expected.iter().map(|t| format_type(t, prog)).collect::<Vec<_>>().join("; ")
            ),
        )
        .help("give it a value of the expected type, convert it (e.g. `x as int`), or change the declared type")
    }

    pub fn type_mismatch_kinds(&self, span: Span, actual: &PType, expected: &[TypeKind], prog: &Program) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "got type: {}, expected: {}",
                format_type(actual, prog),
                expected.iter().map(|k| k.name()).collect::<Vec<_>>().join(", ")
            ),
        )
        .help("give it a value of one of the expected types, or convert it (`x as int`)")
    }

    pub fn missing_named_tuple_entry(&self, span: Span, tuple: &PType, field: &str, prog: &Program) -> Diagnostic {
        Diagnostic::new(span, format!("named tuple type {} has no '{}' field", tuple.original_repr(prog), field))
            .help("use one of the tuple's field names, or add the field to its type")
    }

    pub fn out_of_bounds_tuple_access(&self, span: Span, tuple: &PType, index: &str, prog: &Program) -> Diagnostic {
        Diagnostic::new(span, format!("tuple type {} has no '{}' field", tuple.original_repr(prog), index))
            .help("tuple fields are numbered from 0; use an index below the tuple's size")
    }

    pub fn incomparable_types(&self, span: Span, lhs: &PType, rhs: &PType, prog: &Program) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("types {} and {} are incomparable", lhs.original_repr(prog), rhs.original_repr(prog)),
        )
        .help("compare values of the same type; convert one side first (`x as int`)")
    }

    pub fn misplaced_this(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "keyword 'this' used outside a machine")
            .help("`this` is only defined inside a machine; pass the machine in as a parameter instead")
    }

    pub fn binop_type_mismatch(&self, op_span: Span, op: &str, lhs: &PType, rhs: &PType, prog: &Program) -> Diagnostic {
        Diagnostic::new(
            op_span,
            format!(
                "operator '{}' requires both operands to be int or both float; got {} and {}",
                op,
                lhs.original_repr(prog),
                rhs.original_repr(prog)
            ),
        )
        .help("convert one operand so both match, e.g. `a + (b as float)`")
    }

    pub fn more_than_one_parameter_for_handlers(&self, span: Span, count: usize) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("entry, exit, do, and goto handlers take at most 1 parameter; function expects {}", count),
        )
        .help("take a single parameter; send a tuple payload and read its fields")
    }

    pub fn illegal_choose_sub_expr_type(&self, span: Span, t: &PType, prog: &Program) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "'choose' expects int (max value) or a collection (seq, set, or map); got type {}",
                t.original_repr(prog)
            ),
        )
        .help("pass an int bound (`choose(10)`) or a seq, set or map (`choose(s)`)")
    }

    pub fn illegal_choose_sub_expr_value(&self, span: Span, n: i64) -> Diagnostic {
        Diagnostic::new(span, format!("'choose' expects at most 10000 choices; got {}", n))
            .help("choose from a smaller range, or choose in two steps (a bucket, then a value in it)")
    }

    pub fn illegal_function_used_in_spec(&self, span: Span, fun: &str, monitor: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "function '{}' is non-deterministic or has side effects (new/send/receive/announce); cannot be called from spec monitor '{}'",
                fun, monitor
            ),
        )
        .help("spec monitors must be deterministic: compute the value in a machine and pass it in the event payload")
    }

    pub fn emitted_null_event(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "cannot send null events")
            .help("send a declared event; check that the event variable is set before sending")
    }

    pub fn missing_start_state(&self, span: Span, machine: &str) -> Diagnostic {
        Diagnostic::new(span, format!("machine '{}' has no start state", machine))
            .help("mark one state `start`, e.g. `start state Init { ... }`")
    }

    pub fn changed_state_mid_transition(&self, span: Span, fun: &str) -> Diagnostic {
        Diagnostic::new(span, format!("function '{}' is used as a transition handler but might change state here", fun))
        .help("move the goto or raise out of the function into the handler, or call the function from an `entry` block instead")
    }

    pub fn invalid_string_expr_format(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "string format placeholders must contain only digits; escape braces by doubling them")
        .help(r#"use numbered placeholders like `format("{0} and {1}", a, b)`; write `{{` and `}}` for literal braces"#)
    }

    pub fn invalid_bind(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid bind: {}", msg))
    }

    pub fn not_closed(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("module not closed: {}", msg))
            .help("add machines to the module that implement every interface it creates")
    }

    pub fn no_main(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("illegal main machine: {}", msg))
            .help("name a machine of the module as main: `test t [main=Main]: { Main, ... };`")
    }

    pub fn invalid_assert_event(&self, span: Span, monitor: &str, event: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!(
                "invalid assert: event '{}' in observes set of '{}' is not in the sends set of the module",
                event, monitor
            ),
        )
        .help("add the event to what the module sends, or remove it from the monitor's observes list")
    }

    pub fn invalid_assert_monitor(&self, span: Span, monitor: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid assert: spec monitor '{}' is already attached in the module", monitor))
            .help("attach the monitor once; remove the duplicate `assert`")
    }

    pub fn invalid_hide_event(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid hide event: {}", msg))
    }

    pub fn invalid_hide_interface(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid hide interface: {}", msg))
    }

    pub fn invalid_rename(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid rename: {}", msg))
    }

    pub fn invalid_composition(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("invalid composition: {}", msg))
    }

    pub fn internal_error(&self, span: Span, msg: &str) -> Diagnostic {
        Diagnostic::new(span, format!("internal compiler error (please report): {}", msg)).help(
            "this is a snappea bug; please report it with the model at https://github.com/crowecawcaw/snappea/issues",
        )
    }

    pub fn two_start_states(&self, span: Span, machine: &str, first: &str, second: &str) -> Diagnostic {
        Diagnostic::new(span, format!("machine '{}' has two start states: '{}' and '{}'", machine, first, second))
            .help("keep `start` on exactly one state")
    }

    pub fn value_out_of_range(&self, span: Span, ty: &str) -> Diagnostic {
        Diagnostic::new(span, format!("value is out of range for type '{}'", ty)).help("use a value that fits the type")
    }

    pub fn null_transition_in_monitor(&self, span: Span, monitor: &str) -> Diagnostic {
        Diagnostic::new(span, format!("cannot transition on null event in spec monitor '{}'", monitor))
            .help("spec monitors only react to observed events; transition on a declared event")
    }

    pub fn illegal_monitor_operation(&self, span: Span, monitor: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("'$', '$$', 'this', 'new', 'send', 'announce', 'receive', and 'pop' are not allowed in spec monitor '{}'", monitor),
        )
        .help("spec monitors only observe: move this into a machine, and announce what the monitor needs")
    }

    pub fn deferred_event_in_monitor(&self, span: Span, monitor: &str) -> Diagnostic {
        Diagnostic::new(span, format!("events cannot be deferred in spec monitor '{}'", monitor))
            .help("remove the `defer`; `ignore` the event or handle it in this state")
    }

    pub fn not_all_paths_return(&self, span: Span, fun: &str) -> Diagnostic {
        Diagnostic::new(span, format!("not all paths in function '{}' return a value", fun))
            .help("add a `return` at the end of the function, or in every branch")
    }

    pub fn expected_monitor(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(span, format!("expected a spec monitor; got '{}'", name))
            .help("name a `spec` machine here, not a regular machine")
    }

    pub fn raise_in_non_void(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "'raise' is only allowed in functions that do not return a value")
            .help("raise from a function without a return type, or return a value and let the handler raise")
    }

    pub fn change_state_in_non_void(&self, span: Span) -> Diagnostic {
        Diagnostic::new(span, "state changes are only allowed in functions that do not return a value")
            .help("goto from a function without a return type, or return a value and let the handler goto")
    }

    pub fn duplicate_receive_case(&self, span: Span, event: &str) -> Diagnostic {
        Diagnostic::new(span, format!("event '{}' appears twice in receive statement", event))
            .help("handle each event once per receive; merge the two cases")
    }

    pub fn bare_loop_control_flow(&self, span: Span, stmt: &str) -> Diagnostic {
        Diagnostic::new(span, format!("'{}' cannot appear outside a loop body", stmt))
            .help("move it inside a `while` or `foreach` loop, or use `return` to leave the function")
    }

    pub fn exit_function_cannot_take_parameters(&self, span: Span, count: usize) -> Diagnostic {
        Diagnostic::new(span, format!("exit handlers cannot take parameters; function expects {}", count))
            .help("remove the parameters; exit handlers receive no payload")
    }

    pub fn duplicate_bindings(&self, span: Span, name: &str) -> Diagnostic {
        Diagnostic::new(
            span,
            format!("interface or machine '{}' is mentioned or bound multiple times in the module", name),
        )
        .help("mention each machine and interface once per module expression")
    }

    pub fn spec_observes_incomplete_warning(&self, span: Span, event: &str, machine: &str) -> String {
        format!(
            "[Warning:] [{}] event '{}' is not in the observes list of spec monitor '{}'; the handler will never be triggered",
            self.loc(span),
            event,
            machine
        )
    }
}

/// Levenshtein distance over chars.
fn edit_distance(a: &str, b: &str) -> usize {
    let b: Vec<char> = b.chars().collect();
    let mut prev: Vec<usize> = (0..=b.len()).collect();
    for (i, ca) in a.chars().enumerate() {
        let mut cur = vec![i + 1];
        for (j, cb) in b.iter().enumerate() {
            cur.push((prev[j] + usize::from(ca != *cb)).min(prev[j + 1] + 1).min(cur[j] + 1));
        }
        prev = cur;
    }
    prev[b.len()]
}