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) } }