aboutsummaryrefslogtreecommitdiffhomepage
path: root/notes/compile_time_user_checks.txt
blob: 3dd0ba082e5418ce0cfdff407257c2ebe11c17f9 (plain)
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

Compile-time user checks
========================

It would be nice to be able to add custom checks, that get enforced
by the compiler. For example:

* Value conditions:
    - "If parameter 'a' is non-none, then parameter 'b' must also be non-none"
* Local AST conditions:
    - "Parameter 'a' must come from 'get_a()'"
    - "The return value must be passed immediately to 'take_rv()'"
* Top-level AST conditions:
    - "If you define a global constant 'a' of TypeXYZ, then you should
       also define a function 'a_compare()'"
* Temporal conditions:
    - "The return value must be used"
    - "The return value must in all code paths (including/excluding
       exceptions) be passed to 'take_rv()'"


Requirements
------------

Cross-module checks can be really useful.

- For "error" level checks, this requires strict versioning.
- For "warning" level checks, this could have two since-versions:
    check Xyz since 1.2 warning_since 1.0 {
        ...
    }

How should the checks be defined?

- Take inspiration from "declarative-imperative" API's like that of
  EasyMock in the Java world.
- Take inspiration from XPath.
- Some things will need states


Syntax idea
-----------

Checks for usages of interfaces:

    # Checks can apply to a top-level (types, functions and data)
    # Value condition
    check SomeType.do_stuff since 0.1 {
        # "If parameter 'a' is non-none, then parameter 'b' must also be non-none"
        if parameter("a").maybe_not_none() and parameter("b").maybe_none() {
            error("'b' must always be non-none if 'a' might be non-none.")
        }
    }

    # Local AST conditions
    check SomeType.do_stuff since 0.1 {
        # "Parameter 'a' must come from 'get_a()'"
        if not parameter("a").expr().is_call_to("get_a") {
            error("Parameter 'a' must come from 'get_a()'")
        }
        # "The return value must be passed immediately to 'take_rv()'"
        if not return_value().target().is_parameter_to("take_rv", "a") {
            error("The return value must be passed immediately to 'take_rv()'")
        }
    }

    # Top-level AST condition
    check TypeXYZ {
        # "If you define a global constant 'a' of TypeXYZ, then you should
        #   also define a function 'a_compare()'"
        ConstantDef def = definition()
        if def.is_global().is_constant() {
            # the concatenations operation needs an arena!
            string fname = def.name() + "_compare"
            Function f = def.module().find(fname)
            if f == none {
                error("'" + fname + "()' must also be defined")
            }
        }
    }

    # Temporal condition
    check SomeType.get_value since 0.1 {
        # "The return value must be used"
        if return_value().is_unused() {
            error("Return value must be used")
        }
    }

    check SomeType.get_value since 0.1 {
        # "The return value must in all code paths (including/excluding
        #  exceptions) be passed to 'take_rv()'"
        if not return_value().all_paths(.exceptions=false).is_passed_to(.function("take_rv").param("x")) {
            error("Return value must be passed to 'take_rv()'")
        }
    }

Self-checks for interfaces and for top-level implementation code:

    # No since-version!
    # It's NOT required to specify a top-level.
    check {
        Functions fs = type("SomeType").functions()

        if fs.begins_with("get_").count() != fs.begins_with("set_").count() {
            error("Different number of get_ and set_ methods")
        }
        # XXX these kinds of checks wouldn't be a very good to check for *usage checks*, which are since-versioned!
    }

Self-checks inside a function:

    func f(Thing t)
    {
        if (t.check_fluffiness(0) and t.name == "Box") {
            ...
        } else if (t.check_fluffiness(1) and t.name == "Hat") {
            ...
        } else if (t.check_fluffiness(2) and t.near == "Pillow") { # <--- oops, typo! ("near" instead of "name")
            ...
        }
        ...
        check {
            StatementList sl = calls("check_fluffiness").statement(.if_stmt)
            Matcher m = sl.first().matcher_without_literals()
            assert sl.all_matches(m)
        }
    }