diff --git a/py/dead_dml_methods.py b/py/dead_dml_methods.py index 6083bf0e..55bff8e0 100644 --- a/py/dead_dml_methods.py +++ b/py/dead_dml_methods.py @@ -91,14 +91,14 @@ def traverse_ast(ast): if any(stmt.kind == 'error' for stmt in body.args[0]): # poisoned method, apparently meant to be dead ignored = True - yield (ast.site.lineno, ast.args[4].lineno, ast.args[0], ignored) + yield (ast.site.lineno, ast.args[5].lineno, ast.args[0], ignored) elif ast.kind == 'sharedmethod': - body = ast.args[6] + body = ast.args[7] if body is None: # abstract method, no code generated return assert body.kind == 'compound' - yield (ast.site.lineno, ast.args[7].lineno, ast.args[0], False) + yield (ast.site.lineno, ast.args[8].lineno, ast.args[0], False) elif ast.kind in {'toplevel_if', 'hashif'}: (_, t, f) = ast.args for block in [t, f]: diff --git a/py/dml/dmlparse.py b/py/dml/dmlparse.py index c1814cea..3d74341f 100644 --- a/py/dml/dmlparse.py +++ b/py/dml/dmlparse.py @@ -546,7 +546,7 @@ def object_method_noinparams(t): body = t[6] t[0] = ast.method(site(t), name, (inp, outp, throws, [], body), - t[5], t[2], lex_end_site(t, -1)) + t[5], t[2], False, lex_end_site(t, -1)) @prod_dml12 @@ -573,7 +573,7 @@ def object_method(t): body = t[10] t[0] = ast.method(site(t), name, (inp, outp, throws, [], body), - t[9], t[2], lex_end_site(t, -1)) + t[9], t[2], False, lex_end_site(t, -1)) def method_qualifiers_check(site, qualifiers, inp, outp, throws, default): @@ -598,22 +598,47 @@ def method_qualifiers_check(site, qualifiers, inp, outp, throws, default): "startup methods may not be declared 'default'")) return (inp, outp) +@prod_dml14 +def maybe_colon_yes(t): + '''maybe_colon : COLON''' + if not site(t).provisional_enabled(provisional.explicit_method_decls): + report(ESYNTAX(site(t), ':', "expected '=' or 'default'")) + t[0] = False + else: + t[0] = True + +@prod +def maybe_colon_no(t): + '''maybe_colon : ''' + t[0] = False @prod_dml14 def object_method(t): - '''method : method_qualifiers METHOD objident method_params_typed maybe_default compound_statement''' + '''method : method_qualifiers METHOD objident method_params_typed maybe_colon maybe_default compound_statement''' name = t[3] (inp, outp, throws) = t[4] - body = t[6] + body = t[7] + (inp, outp) = method_qualifiers_check(site(t), t[1], inp, outp, throws, + t[6]) + t[0] = ast.method(site(t), name, + (inp, outp, throws, t[1], body), + t[6], False, t[5], lex_end_site(t, -1)) + +@prod_dml14 +def object_method_abstract(t): + '''method : method_qualifiers METHOD objident method_params_typed SEMI''' + name = t[3] + (inp, outp, throws) = t[4] + body = None (inp, outp) = method_qualifiers_check(site(t), t[1], inp, outp, throws, - t[5]) + False) t[0] = ast.method(site(t), name, (inp, outp, throws, t[1], body), - t[5], False, lex_end_site(t, -1)) + True, False, None, site(t, 5)) @prod_dml14 def object_inline_method(t): - '''method : INLINE METHOD objident method_params_maybe_untyped maybe_default compound_statement''' + '''method : INLINE METHOD objident method_params_maybe_untyped maybe_colon maybe_default compound_statement''' name = t[3] (inp, outp, throws) = t[4] if all(typ for (_, asite, name, typ) in inp): @@ -621,10 +646,10 @@ def object_inline_method(t): # We forbid it as a way to strongly discourage unneeded use of inline. report(ESYNTAX(site(t, 2), 'inline', 'only use inline if there are untyped arguments')) - body = t[6] + body = t[7] t[0] = ast.method(site(t), name, (inp, outp, throws, [], body), - t[5], False, lex_end_site(t, -1)) + t[6], False, t[5], lex_end_site(t, -1)) @prod_dml12 @@ -724,12 +749,13 @@ def template_statement_obj(t): @prod_dml14 def template_statement_shared_method(t): '''template_stmt : SHARED method_qualifiers METHOD shared_method''' - (name, (inp, outp, throws), overridable, body, rbrace_site) = t[4] + (name, (inp, outp, throws), overridable, explicit_decl, body, + rbrace_site) = t[4] default = overridable and body is not None (inp, outp) = method_qualifiers_check(site(t), t[2], inp, outp, throws, default) t[0] = [ast.sharedmethod(site(t), name, inp, outp, throws, t[2], - overridable, body, rbrace_site)] + overridable, explicit_decl, body, rbrace_site)] @prod_dml14 def template_statement_shared_hook(t): @@ -759,23 +785,24 @@ def method_qualifiers(t): @prod_dml12 def trait_method(t): '''trait_method : METHOD shared_method''' - (name, (inp, outp, throws), overridable, body, rbrace_site) = t[2] + (name, (inp, outp, throws), overridable, explicit_decl, body, + rbrace_site) = t[2] t[0] = ast.sharedmethod(site(t), name, inp, outp, throws, [], overridable, - body, rbrace_site) + explicit_decl, body, rbrace_site) @prod def shared_method_abstract(t): '''shared_method : ident method_params_typed SEMI''' - t[0] = (t[1], t[2], True, None, site(t, 3)) + t[0] = (t[1], t[2], True, False, None, site(t, 3)) @prod def shared_method_default(t): - '''shared_method : ident method_params_typed DEFAULT compound_statement''' - t[0] = (t[1], t[2], True, t[4], lex_end_site(t, -1)) + '''shared_method : ident method_params_typed maybe_colon DEFAULT compound_statement''' + t[0] = (t[1], t[2], True, t[3], t[5], lex_end_site(t, -1)) @prod def shared_method_final(t): - '''shared_method : ident method_params_typed compound_statement''' - t[0] = (t[1], t[2], False, t[3], lex_end_site(t, -1)) + '''shared_method : ident method_params_typed maybe_colon compound_statement''' + t[0] = (t[1], t[2], False, t[3], t[4], lex_end_site(t, -1)) @prod_dml12 def trait_param(t): diff --git a/py/dml/messages.py b/py/dml/messages.py index 36f9657f..8107ea41 100644 --- a/py/dml/messages.py +++ b/py/dml/messages.py @@ -198,9 +198,9 @@ def log(self): class EAMETH(DMLError): """ - An abstract method cannot override another method. + A shared abstract method cannot override another method. """ - fmt = "abstract method %s overrides existing method" + fmt = "shared abstract method %s overrides existing method" def __init__(self, site, prev_site, name): DMLError.__init__(self, site, name) @@ -247,6 +247,13 @@ def log(self): self.print_site_message( self.decl_site, "abstract declaration") +class EABSMETH(DMLError): + """ + An (abstractly) declared method never has any definition made for it. + """ + version = "1.4" + fmt = "declared method %s is never implemented" + class EIMPORT(DMLError): """ The file to imported could not be found. Use the `-I` @@ -1134,7 +1141,7 @@ class EAUTOPARAM(DMLError): library, and they may not be overridden.""" fmt = "bad declaration of automatic parameter '%s'" -class ENOVERRIDE(DMLError): +class ENOVERRIDEPARAM(DMLError): """When the `explict_param_decls` provisional feature is enabled, parameter definitions written using `=` and `default` are only accepted if the parameter has already been declared. @@ -1154,7 +1161,7 @@ def log(self): "enabled by the explicit_param_decls provisional feature") -class EOVERRIDE(DMLError): +class EOVERRIDEPARAM(DMLError): """When the `explict_param_decls` provisional feature is enabled, any parameter declared via `:=` or `:default` may not already have been declared. This means `:=` or `:default` syntax can't be used @@ -1271,6 +1278,42 @@ def log(self): if self.othersite: self.print_site_message(self.othersite, "conflicting definition") +class ENOVERRIDEMETH(DMLError): + """When the `explict_method_decls` provisional feature is enabled, method + definitions written using `{ ... }` and `default { ... }` are only accepted + if the method has already been declared. + + To declare and define a new method not already declared, use the `:{ ... }` + or `:default { ... }` syntax. + """ + fmt = ("method '%s' not declared previously." + " To declare and define a new method, use the ':%s{...}' syntax.") + + def log(self): + from . import provisional + DMLError.log(self) + prov_site = self.site.provisional_enabled( + provisional.explicit_method_decls) + self.print_site_message( + prov_site, + "enabled by the explicit_method_decls provisional feature") + +class EOVERRIDEMETH(DMLError): + """When the `explict_method_decls` provisional feature is enabled, + any method declared via `:{ ... }` or `:default { ... }` may not already + have been declared. This means `:{ ... }` or `:default { ... }` syntax + can't be used to override existing parameter declarations (not even those + lacking a definition of the parameter.) + """ + fmt = ("the method '%s' has already been declared " + + "(':%s{ ... }' syntax may not be used for method overrides)") + def __init__(self, site, other_site, name, token): + super().__init__(site, name, token) + self.other_site = other_site + def log(self): + DMLError.log(self) + self.print_site_message(self.other_site, "existing declaration") + class EIMPLMEMBER(DMLError): """ A method in an `implement` object corresponds to a struct member diff --git a/py/dml/provisional.py b/py/dml/provisional.py index 8a64c6d7..621cdfb2 100644 --- a/py/dml/provisional.py +++ b/py/dml/provisional.py @@ -84,6 +84,60 @@ class explicit_param_decls(ProvisionalFeature): short = "Require := syntax for defining new params" stable = True +@feature +class explicit_method_decls(ProvisionalFeature): + ''' + This feature extends the DML syntax for methods to distinguish between an + intent to declare a new method, and an intent to override an existing + method/provide a definition for an abstract method. + This distinction allows DML to capture misspelled parameter overrides as + compile errors. + + This provisional feature introduces new syntax for the following purposes: + * Abstract method declarations + ``` + method m(...) [-> (...)] [throws]; + ``` + + This declaration establishes a requirement that a method of that name and + signature is defined in the same object as the abstract method + declaration. This is similar to the existing abstract `shared` method + declaration, but unlike abstract `shared` methods have no restrictions + or semantic implications beyond that (except for the fact that it + declares the method to exist.) In other words, it's semantically + analagous to untyped abstract parameter declarations (`param p;`). + + * Simultaneously declaring and defining a new method + ``` + method m(...) [-> (...)] [throws] :{ ... } + method m(...) [-> (...)] [throws] :default { ... } + ``` + + DMLC rejects a declaration of this form if the method has already been + declared, because this form signifies that the declaration was not + intended as an override. + + `explicit_metod_decls` also changes the meaning of the traditional form + of method definitions (e.g. `method m() {}` or `method m() default {}`) + such that DMLC will reject them if the method has not been declared + previously (either abstractly or with an overridable definition.) + + In some rare cases, you may need to declare a method without + knowing if it's an override or a new declaration. In this case, one + can accompany an overriding definition (e.g. `method m() {}` + or `method() default {}`) with an abstract method declaration (e.g. + `method m();`) in the same scope/rank. This marks that the method + definition may either be for a previously declared method or a new method + entirely, and no error will be printed. + + Enabling the `explicit_method_decls` feature in a file only affects + the method definitions specified in that file; in other words, it will not + require other files to use the `:{` syntax in order to declare novel + methods. + ''' + short = "Require :{ ... } syntax for defining new methods" + stable = False + @feature class simics_util_vect(ProvisionalFeature): diff --git a/py/dml/structure.py b/py/dml/structure.py index b3ee378b..f1ac8b5c 100644 --- a/py/dml/structure.py +++ b/py/dml/structure.py @@ -477,10 +477,11 @@ def wrap_sites(spec, issite, tname): for stmt in shallow: asttype = stmt.kind if asttype == 'method': - (_, site, name, value, overridable, export, rsite) = stmt + (_, site, name, value, overridable, explicit_decl, export, + rsite) = stmt shallow_wrapped.append(ast.method( TemplateSite(site, issite, tname), name, value, - overridable, export, rsite)) + overridable, explicit_decl, export, rsite)) elif asttype == 'error': (_, site, msg) = stmt shallow_wrapped.append( @@ -676,14 +677,14 @@ def decl_is_default(decl): if not declared_as_override and parent_ranks: [parent, *_] = (parent for (parent_rank, parent) in params if parent_rank in parent_ranks) - report(EOVERRIDE(type_info.site, parent.site, name, - 'default' if is_default else '=')) + report(EOVERRIDEPARAM(type_info.site, parent.site, name, + 'default' if is_default else '=')) if not declared_as_override and rank in decls: - report(EOVERRIDE(type_info.site, decls[rank].site, name, - 'default' if is_default else '=')) + report(EOVERRIDEPARAM(type_info.site, decls[rank].site, name, + 'default' if is_default else '=')) elif (not parent_ranks and declared_as_override and rank not in decls): - report(ENOVERRIDE( + report(ENOVERRIDEPARAM( p.site, name, 'default' if is_default else '=')) [(rank0, param0)] = superior @@ -711,9 +712,9 @@ def decl_is_default(decl): def typecheck_method_override(m1, m2, location): '''check that m1 can override m2''' assert m1.kind == m2.kind == 'method' - (_, (inp1, outp1, throws1, qualifiers1, _), _, _, _) \ + (_, (inp1, outp1, throws1, qualifiers1, _), _, _, _, _) \ = m1.args - (_, (inp2, outp2, throws2, qualifiers2, _), _, _, _) \ + (_, (inp2, outp2, throws2, qualifiers2, _), _, _, _, _) \ = m2.args (independent1, startup1, memoized1) = ('independent' in qualifiers1, 'startup' in qualifiers1, @@ -783,14 +784,10 @@ def typecheck_method_override(m1, m2, location): report(PINARGTYPE(m1.site, 'method')) def qualifier_check(qualifier_name, qualifier1, qualifier2): - if qualifier1 > qualifier2: + if qualifier1 != qualifier2: raise EMETH(m1.site, m2.site, - (f"overriding method is declared {qualifier_name}, " - + "but the overridden method is not")) - elif qualifier1 < qualifier2: - raise EMETH(m1.site, m2.site, - (f"overridden method is declared {qualifier_name}, " - + "but the overriding method is not")) + (f"one declaration is qualified as {qualifier_name}, " + + "but the other is not")) qualifier_check('independent', independent1, independent2) qualifier_check('startup', startup1, startup2) @@ -810,22 +807,25 @@ def report_poverride(sup, inf, obj_specs): else: report(POVERRIDE_IMPORT(sup_obj.site, inf.desc.text)) -def sort_method_implementations(implementations, obj_specs): - if dml.globals.dml_version == (1, 2) and len(implementations) == 2: +def sort_method_declarations(declarations, obj_specs): + if dml.globals.dml_version == (1, 2): # Backward compatibility: If there is exactly one default and # one non-default implementation, then disregard template # instantiation relations. - [m1, m2] = implementations - # create fake ranks to make sure methods end up in the right order - if m1.overridable and not m2.overridable: - if logging.show_porting: - report_poverride(m2.obj_spec.rank, m1.obj_spec.rank, obj_specs) - m2.rank = Rank({m1.rank}, m2.rank.desc) - elif not m1.overridable and m2.overridable: - if logging.show_porting: - report_poverride(m1.rank, m2.rank, obj_specs) - m1.rank = Rank({m2.rank}, m1.rank.desc) - return traits.sort_method_implementations(implementations) + implementations = [impl for impl in declarations if not impl.abstract] + if len(implementations) == 2: + [m1, m2] = implementations + # create fake ranks to make sure methods end up in the right order + if m1.overridable and not m2.overridable: + if logging.show_porting: + report_poverride(m2.obj_spec.rank, m1.obj_spec.rank, + obj_specs) + m2.rank = Rank({m1.rank}, m2.rank.desc) + elif not m1.overridable and m2.overridable: + if logging.show_porting: + report_poverride(m1.rank, m2.rank, obj_specs) + m1.rank = Rank({m2.rank}, m1.rank.desc) + return traits.sort_method_declarations(declarations) def merge_subobj_defs(def1, def2, parent): (objtype, name, arrayinfo, obj_specs1) = def1 @@ -1414,51 +1414,95 @@ def wrap_method_body_in_try(site, overridden_site, obj, name, body, class ObjMethodHandle(traits.MethodHandle): shared = False def __init__(self, method_ast, obj_spec): - (name, (_, _, throws, _, _), overridable, _, _) = method_ast.args + (name, (inp, outp, throws, qualifiers, body), overridable, + _extern, explicit_decl, _rbrace_site) = method_ast.args super(ObjMethodHandle, self).__init__( - method_ast.site, name, obj_spec, overridable) + method_ast.site, name, obj_spec, overridable, body is None, + explicit_decl, inp, outp, throws, 'independent' in qualifiers, + 'startup' in qualifiers, 'memoized' in qualifiers) self.method_ast = method_ast - self.throws = throws class TraitMethodHandle(traits.MethodHandle): shared = True def __init__(self, trait_method, obj_spec): super(TraitMethodHandle, self).__init__( - trait_method.site, trait_method.name, - obj_spec, trait_method.overridable) + trait_method.site, trait_method.name, obj_spec, + trait_method.overridable, False, None, trait_method.inp, + trait_method.outp, trait_method.throws, trait_method.independent, + trait_method.startup, trait_method.memoized) self.trait_method = trait_method - self.throws = trait_method.throws -def process_method_implementations(obj, name, implementations, - shared_impl_traits, obj_specs, - vtable_nothrow_dml14): - # A method can have both shared and non-shared implementations. +class AbstractTraitMethodHandle(traits.MethodHandle): + shared = True + def __init__(self, name, vtable_trait): + (site, *sig) = vtable_trait.vtable_methods[name] + obj_spec = dml.globals.templates[vtable_trait.name].spec + super(AbstractTraitMethodHandle, self).__init__( + site, name, obj_spec, True, True, None, *sig) + + + +def process_method_declarations(obj, name, declarations, + shared_impl_traits, shared_absdecl_traits, + obj_specs, vtable_nothrow_dml14): + # A method can have both shared and non-shared declarations. # We will create a MethodHandle object for either, which - # sort_method_implementations() uses to resolve override order. + # sort_method_declarations() uses to resolve override order. unshared_methods = [ ObjMethodHandle(method_ast, obj_spec) - for (obj_spec, method_ast) in implementations] + for (obj_spec, method_ast) in declarations] shared_methods = [ TraitMethodHandle( impl_trait.method_impls[name], dml.globals.templates[impl_trait.name].spec) for impl_trait in shared_impl_traits] + shared_abstract_methods = [ + AbstractTraitMethodHandle(name, vtable_trait) + for vtable_trait in shared_absdecl_traits] - (default_map, method_order) = sort_method_implementations( - unshared_methods + shared_methods, obj_specs) + (default_map, decl_ancestry_map, method_order, + abstract_decls) = sort_method_declarations( + unshared_methods + shared_methods + shared_abstract_methods, obj_specs) location = Location(obj, static_indices(obj)) - impl_to_method = {} - for (default_level, impl) in reversed(list(enumerate( - method_order))): + nonshared_impls = [] + + for impl in method_order: if impl.shared: if default_map[impl]: # shared method overrides a non-shared method report(ETMETH( default_map[impl][0].site, impl.site, name)) - # handled separately - continue + else: + nonshared_impls.append(impl) + + if not nonshared_impls: + if shared_methods: + shared_impl_sig = shared_methods[0].signature + for decl in abstract_decls: + if decl.shared: + # handled separately + continue + + try: + traits.typecheck_method_override(shared_impl_sig, + decl.signature) + except DMLError as e: + report(e) + return None + else: + assert abstract_decls + for decl in abstract_decls: + # We favor reporting EABSTEMPLATE (done by the caller) + # over EABSMETH + if not decl.shared: + report(EABSMETH(decl.site, name)) + + return None + + impl_to_method = {} + for (default_level, impl) in reversed(list(enumerate(nonshared_impls))): defaults = default_map[impl] if len(defaults) == 0: default = InvalidDefault(traits.NoDefaultSymbol(impl.site)) @@ -1474,7 +1518,7 @@ def process_method_implementations(obj, name, implementations, default = InvalidDefault(traits.AmbiguousDefaultSymbol( [m.site for m in defaults])) (name, (inp_ast, outp_ast, throws, qualifiers, body), _, - _, rbrace_site) = impl.method_ast.args + _, _, rbrace_site) = impl.method_ast.args independent = 'independent' in qualifiers startup = 'startup' in qualifiers memoized = 'memoized' in qualifiers @@ -1491,20 +1535,11 @@ def process_method_implementations(obj, name, implementations, impl.site, vtable_nothrow_dml14, obj, name, body, rbrace_site) throws = False - for overridden in default_map[impl]: - if not overridden.overridable: - raise EDMETH(impl.site, overridden.site, name) - # the override of trait ASTs is checked later, by - # traits.typecheck_method_override - if not overridden.shared: - # captured with ETMETH above - assert not impl.shared - typecheck_method_override(impl.method_ast, - overridden.method_ast, - location) + + for ancestor_decl in decl_ancestry_map[impl]: if (impl.site.dml_version() == (1, 2) and throws - and not overridden.throws - and overridden.site.dml_version() == (1, 4)): + and not ancestor_decl.throws + and ancestor_decl.site.dml_version() == (1, 4)): # If a 1.4 library file defines an overrideable # non-throwing method, and a 1.2 device overrides # this, then assume that the method was never @@ -1513,12 +1548,11 @@ def process_method_implementations(obj, name, implementations, # We modify the override to nothrow, and patch it # to catch any exceptions. body = wrap_method_body_in_try( - impl.site, overridden.site, + impl.site, ancestor_decl.site, obj, name, body, rbrace_site) throws = False - elif throws != overridden.throws: - if (dml.globals.dml_version == (1, 2) - and not throws): + elif throws != ancestor_decl.throws: + if dml.globals.dml_version == (1, 2) and not throws: # Permit a nonthrowing method to override # a throwing 1.2 method, with no warning. # This is needed for some common standard @@ -1527,12 +1561,47 @@ def process_method_implementations(obj, name, implementations, # 1.2 and 1.4, pass else: - raise EMETH(impl.site, overridden.site, - "different nothrow annotations") + annotation = ("no"*(dml.globals.dml_version != (1, 4)) + + "throw") + raise EMETH(impl.site, ancestor_decl.site, + f"different {annotation} annotations") + + for overridden in default_map[impl]: + if not overridden.overridable: + raise EDMETH(impl.site, overridden.site, name) + # the override of trait ASTs is checked later, by + # traits.typecheck_method_override + if not overridden.shared: + # captured with ETMETH above + assert not impl.shared + typecheck_method_override(impl.method_ast, + overridden.method_ast, + location) + + # Check compatibility with all non-shared abstract declarations against + # the highest-ranked implementation + if default_level == 0: + for decl in abstract_decls: + if decl.shared: + # handled separately + continue + + typecheck_method_override(impl.method_ast, + decl.method_ast, + location) + + if (not decl.throws and throws + and impl.dml_version() != (1, 4)): + + body = wrap_method_body_in_try( + impl.site, decl.site, obj, name, body, rbrace_site) + throws = False template = (impl.obj_spec.parent_template if isinstance(impl.obj_spec, InstantiatedTemplateSpec) else None) + if isinstance(body, bool): + report(ICE(impl.site, f"wut: {repr(impl)}")) method = mkmethod(impl.site, rbrace_site, location, obj, name, inp_ast, @@ -1547,8 +1616,8 @@ def process_method_implementations(obj, name, implementations, obj.template_method_impls[(template, name)] = method if dml.globals.dml_version == (1, 2): - for (_, method_ast) in implementations: - (_, msite, _, _, _, exported, _) = method_ast + for (_, method_ast) in declarations: + (_, msite, _, _, _, exported, _, _) = method_ast if exported: if not method.fully_typed: raise EEXTERN(method.site) @@ -1634,7 +1703,7 @@ def mkobj2(obj, obj_specs, params, each_stmts): _, esite, msg = s raise EERRSTMT(esite, msg or "explicit error") elif s.kind == 'method': - (name, _, _, _, _) = s.args + (name, _, _, _, _, _) = s.args if name not in method_asts: if name in symbols: report(ENAMECOLL(s.site, symbols[name], name)) @@ -1762,7 +1831,7 @@ def mkobj2(obj, obj_specs, params, each_stmts): break else: vtable_nothrow_dml14 = False - implementations = method_asts[name] + declarations = method_asts[name] if (dml.globals.dml_version != (1, 2) and name in { 'register': {'read', 'write', 'read_field', 'write_field'}, @@ -1770,19 +1839,31 @@ def mkobj2(obj, obj_specs, params, each_stmts): 'get', 'set'}, }.get(obj.objtype, set())): if dml.globals.traits[name] not in ancestors: - (_, mast) = implementations[0] + (_, mast) = declarations[0] report(WNOIS(mast.site, name)) + trait_impls = trait_method_impls.get(name, []) + trait_abstract_decls = Set() + # Right now, calculating trait_abstract_decls is only relevant when + # trait_impls is empty, so we don't calculate it otherwise + if not trait_impls: + for t in explicit_traits: + decl = t.member_declaration(name) + if (decl is not None + and t.member_kind(name) == 'method'): + trait_abstract_decls.add(t.vtable_trait(name)) + try: - method = process_method_implementations( - obj, name, implementations, - trait_method_impls.get(name, []), + method = process_method_declarations( + obj, name, declarations, + trait_method_impls.get(name, []), trait_abstract_decls, obj_specs, dml.globals.dml_version == (1, 2) and vtable_nothrow_dml14) except DMLError as e: report(e) else: - obj.add_component(method) + if method is not None: + obj.add_component(method) if logging.show_porting: report_pbefaft(obj, method_asts) diff --git a/py/dml/traits.py b/py/dml/traits.py index 46faa85d..5ab3a702 100644 --- a/py/dml/traits.py +++ b/py/dml/traits.py @@ -9,6 +9,7 @@ import abc import os from . import objects, logging, crep, codegen, toplevel, topsort, compat +from . import provisional from .logging import * from .codegen import * from .symtab import * @@ -40,7 +41,7 @@ def process_trait(site, name, subasts, ancestors, template_symbols): hooks = {} def check_namecoll(name, site): if name in methods: - (othersite, _, _, _, _, _, _, _, _, _) = methods[name] + (othersite, _, _, _, _, _, _, _, _, _, _) = methods[name] raise ENAMECOLL(site, othersite, name) if name in params: (othersite, _) = params[name] @@ -56,7 +57,7 @@ def check_namecoll(name, site): try: if ast.kind == 'sharedmethod': (mname, inp_asts, outp_asts, throws, qualifiers, - overridable, body, rbrace_site) = ast.args + overridable, explicit_decl, body, rbrace_site) = ast.args independent = 'independent' in qualifiers startup = 'startup' in qualifiers memoized = 'memoized' in qualifiers @@ -70,8 +71,8 @@ def check_namecoll(name, site): outp = eval_method_outp(outp_asts, None, global_scope) check_namecoll(mname, ast.site) methods[mname] = (ast.site, inp, outp, throws, independent, - startup, memoized, overridable, body, - rbrace_site) + startup, memoized, overridable, + explicit_decl, body, rbrace_site) elif ast.kind in {'session', 'saved'}: (decls, _) = ast.args for decl_ast in decls: @@ -322,11 +323,14 @@ def mktrait(site, tname, ancestors, methods, params, sessions, hooks, del sessions[name] bad_methods = set() - for (name, (msite, inp, outp, throws, independent, startup, memoized, overridable, - body, rbrace_site)) in list(methods.items()): + for (name, (msite, inp, outp, throws, independent, startup, memoized, + overridable, explicit_decl, body, rbrace_site) + ) in list(methods.items()): + some_coll = False for ancestor in direct_parents: coll = ancestor.member_declaration(name) if coll: + some_coll = True (orig_site, orig_trait) = coll if orig_trait.member_kind(name) != 'method': # cannot override non-method with method @@ -347,14 +351,22 @@ def mktrait(site, tname, ancestors, methods, params, sessions, hooks, report(EDMETH(msite, orig_trait.method_impls[name].site, name)) bad_methods.add(name) + elif explicit_decl: + report(EOVERRIDEMETH(msite, orig_site, name, + 'default '*overridable)) + bad_methods.add(name) elif name not in ancestor_vtables: raise ICE(msite, 'ancestor is overridable but not in vtable') - # Type-checking of overrides is done later, after typedefs # have been populated with all template types. # See Trait.typecheck_methods() + if (body is not None and not some_coll and not explicit_decl + and msite.provisional_enabled(provisional.explicit_method_decls)): + report(ENOVERRIDEMETH(msite, name, 'default '*overridable)) + bad_methods.add(name) + for name in bad_methods: del methods[name] @@ -392,7 +404,8 @@ def typecheck_method_override(left, right): if len(outp0) != len(outp1): raise EMETH(site0, site1, "different number of output arguments") if throws0 != throws1: - raise EMETH(site0, site1, "different nothrow annotations") + annotation = "no"*(dml.globals.dml_version != (1, 4)) + "throw" + raise EMETH(site0, site1, f"different {annotation} annotations") for ((n, t0), (_, t1)) in zip(inp0, inp1): t0 = safe_realtype_unconst(t0) t1 = safe_realtype_unconst(t1) @@ -413,14 +426,10 @@ def typecheck_method_override(left, right): "mismatching types in output argument %d" % (i + 1,)) def qualifier_check(qualifier_name, qualifier0, qualifier1): - if qualifier0 > qualifier1: - raise EMETH(site0, site1, - (f"overriding method is declared {qualifier_name}, " - + "but the overridden method is not")) - elif qualifier0 < qualifier1: + if qualifier0 != qualifier1: raise EMETH(site0, site1, - (f"overridden method is declared {qualifier_name}, " - + "but the overriding method is not")) + (f"one declaration is qualified as {qualifier_name}, " + + "but the other is not")) qualifier_check('independent', independent0, independent1) qualifier_check('startup', startup0, startup1) @@ -493,12 +502,28 @@ def merge_method_impl_maps(site, parents): return merged_impls class MethodHandle(object): - def __init__(self, site, name, obj_spec, overridable): + def __init__(self, site, name, obj_spec, overridable, abstract, + explicit_decl, + inp, outp, throws, independent, startup, memoized): self.site = site self.name = name self.obj_spec = obj_spec self.overridable = overridable self.rank = obj_spec.rank + self.abstract = abstract + self.explicit_decl = explicit_decl + self.inp = inp + self.outp = outp + self.throws = throws + self.independent = independent + self.startup = startup + self.memoized = memoized + + @property + def signature(self): + '''To be used with e.g. typecheck_method_override''' + return (self.site, self.inp, self.outp, self.throws, self.independent, + self.startup, self.memoized) def get_highest_ranks(ranks): '''Given a set of ranks, return the subset of highest unrelated ranks''' @@ -532,53 +557,120 @@ def calc_minimal_ancestry(ranks: frozenset["Rank"]): return minimal_ancestry -def sort_method_implementations(implementations): - '''Given a list of (Rank, ast.method) pairs, return a pair - (default_map, method_order), where default_map is a dict mapping - ast.method to list of ast.method it overrides, and method_order is - a topological ordering of methods based on this graph.''' +def sort_method_declarations(declarations): + '''Given a list of MethodHandle:s, return a tuple + (default_map, decl_ancestry_map, method_order, abstract_decls), where: + * default_map is a dict mapping each non-abstract MethodHandle + to the list of highest-ranking non-abstract MethodHandle:s it overrides + * decl_ancestry_map is a dict mapping each MethodHandle to the list of + highest-ranking MethodHandle:s it overrides + * method_order is a topological ordering of method implementations + based on the graph formed from default_map + * abstract_decls is a list of all abstract declarations of the method + ''' + # Rank -> [non-abstract MethodHandle or None, abstract MethodHandle:s] rank_to_method = {} - for impl in implementations: - if impl.rank in rank_to_method: + for impl in declarations: + [existing_impl, existing_abstracts] = existing = \ + rank_to_method.setdefault(impl.rank, [None, []]) + if impl.abstract: + existing_abstracts.append(impl) + elif existing_impl is not None: # two conflicting method definitions in the same block - raise ENAMECOLL(impl.site, rank_to_method[impl.rank].site, - impl.name) - rank_to_method[impl.rank] = impl + raise ENAMECOLL(impl.site, existing_impl.site, impl.name) + else: + existing[0] = impl + def flattened(t): + (impl, abstracts) = t + return abstracts if impl is None else [impl] + abstracts minimal_ancestry = calc_minimal_ancestry(frozenset(rank_to_method)) + decl_ancestry_map = {} + for (r, anc_ranks) in minimal_ancestry.items(): + anc_decls = [anc_decl + for anc in anc_ranks + for anc_decl in flattened(rank_to_method[anc])] + + decls = flattened(rank_to_method[r]) if r is not None else (None,) + for decl in decls: + decl_ancestry_map[decl] = anc_decls + + method_map_ranks = {} + + def calc_highest_impls(r): + existing = method_map_ranks.get(r) + if existing is not None: + return + + anc_impl_ranks = Set() + some_abstract = False + for anc in minimal_ancestry[r]: + [anc_impl, anc_abstracts] = rank_to_method[anc] + if anc_impl is not None: + anc_impl_ranks.add(anc) + else: + some_abstract = True + calc_highest_impls(anc) + anc_impl_ranks.update(method_map_ranks[anc]) + + if some_abstract: + anc_impl_ranks = get_highest_ranks(anc_impl_ranks) + method_map_ranks[r] = anc_impl_ranks + + calc_highest_impls(None) + for (r, (impl, abstracts)) in rank_to_method.items(): + if (impl is not None and not impl.shared + and impl.site.provisional_enabled( + provisional.explicit_method_decls)): + existing = abstracts or decl_ancestry_map[impl] + if impl.explicit_decl: + if existing: + report(EOVERRIDEMETH(impl.site, existing[0].site, + impl.name, + 'default ' * impl.overridable)) + elif not existing: + report(ENOVERRIDEMETH(impl.site, impl.name, + 'default '*impl.overridable)) + + calc_highest_impls(r) + + # Ancestry graph translated back to method ASTs. Maps method to + # list of default methods. + method_map = { + impl: [rank_to_method[anc][0] for anc in method_map_ranks[r]] + for (r, (impl, _)) in rank_to_method.items() if impl is not None } + method_order = list(reversed(topsort.topsort(method_map))) - if len(minimal_ancestry[None]) > 1: + highest_rank_impls = method_map_ranks[None] + if len(highest_rank_impls) > 1: # There is no single method implementation that overrides all # other implementations def is_default(r): - return rank_to_method[r].overridable + return rank_to_method[r][0].overridable - [r1, r2] = sorted(minimal_ancestry[None], key=is_default)[:2] - raise EAMBINH(rank_to_method[r1].site, - rank_to_method[r2].site, - rank_to_method[r1].name, + [r1, r2] = sorted(highest_rank_impls, key=is_default)[:2] + raise EAMBINH(rank_to_method[r1][0].site, + rank_to_method[r2][0].site, + rank_to_method[r1][0].name, r1.desc, r2.desc, is_default(r1)) - # Ancestry graph translated back to method ASTs. Maps method to - # list of default methods. - method_map = { - m: [rank_to_method[x] for x in minimal_ancestry[r]] - for (r, m) in rank_to_method.items()} - method_order = list(reversed(topsort.topsort(method_map))) - - m = method_order[0] - if (dml.globals.dml_version == (1, 2) - and os.path.basename(m.site.filename()) != 'dml12-compatibility.dml'): - if len(implementations) > 2: + if (dml.globals.dml_version == (1, 2) and method_order + and os.path.basename( + method_order[0].site.filename()) != 'dml12-compatibility.dml'): + m = method_order[0] + if len(method_order) > 2: report(WEXPERIMENTAL( m.site, "more than one level of method overrides")) - if len(implementations) == 2 and m.overridable: + if len(method_order) == 2 and m.overridable: report(WEXPERIMENTAL( m.site, "method with two default declarations")) - return (method_map, method_order) + return (method_map, decl_ancestry_map, method_order, + itertools.chain(*(abstracts + for (_, abstracts) in rank_to_method.values() + if abstracts))) class SubTrait: '''Logic shared between nodes and traits, which both can inherit @@ -701,7 +793,7 @@ def __init__(self, site, name, ancestors, methods, params, sessions, hooks, overridable, body, self, name, ancestor_method_impls.get(name, []), rbrace_site) for (name, (msite, inp, outp, throws, independent, startup, - memoized, overridable, body, rbrace_site)) + memoized, overridable, _, body, rbrace_site)) in list(methods.items()) if body is not None} @@ -723,8 +815,8 @@ def __init__(self, site, name, ancestors, methods, params, sessions, hooks, # methods and parameters that are direct members of this trait's vtable self.vtable_methods = { name: (msite, inp, outp, throws, independent, startup, memoized) - for (name, (msite, inp, outp, throws, independent, startup, memoized, - overridable, _, _)) + for (name, (msite, inp, outp, throws, independent, startup, + memoized, overridable, _, _, _)) in list(methods.items()) if overridable and name not in ancestor_vtables} self.vtable_params = params diff --git a/py/dml/traits_test.py b/py/dml/traits_test.py index e78aca21..34f24d6d 100644 --- a/py/dml/traits_test.py +++ b/py/dml/traits_test.py @@ -44,7 +44,7 @@ def test_one_default_method(self): body = dml.ast.compound(self.site, [], self.site) t = Trait(self.site, 't', set(), {'m': (self.site, [], [], False, False, False, False, True, - body, None)}, + False, body, None)}, {}, {}, {}, {}, {}, {}) ot = ObjTraits(self.dev, {t}, {'m': t}, {}, {}) self.dev.set_traits(ot) diff --git a/test/1.4/errors/T_EMETH.dml b/test/1.4/errors/T_EMETH.dml index 8a1b740d..99f449a9 100644 --- a/test/1.4/errors/T_EMETH.dml +++ b/test/1.4/errors/T_EMETH.dml @@ -23,10 +23,28 @@ template t { } /// ERROR EMETH independent method indep_ret() -> (int) default { return 0; } + + /// ERROR EMETH + method with_abstract_decls(); + + method with_abstract_decls(int i) default {} + + shared method sm() {} } is t; +/// ERROR EMETH +method sm(int i); + +// no error +method sm(); + +/// ERROR EMETH +method with_abstract_decls(bool b); + +method with_abstract_decls(int i) {} + template u is t {} template v1 is u { @@ -62,6 +80,11 @@ group g4 is t { } group g5 is t { + /// ERROR EMETH + method with_abstract_decls(bool b); + + method with_abstract_decls(int i) {} + /// ERROR EMETH independent startup method indep() { } diff --git a/test/1.4/provisional/T_explicit_method_decls.dml b/test/1.4/provisional/T_explicit_method_decls.dml new file mode 100644 index 00000000..bb5e6dac --- /dev/null +++ b/test/1.4/provisional/T_explicit_method_decls.dml @@ -0,0 +1,44 @@ +/* + © 2025 Intel Corporation + SPDX-License-Identifier: MPL-2.0 +*/ + +dml 1.4; + +provisional explicit_method_decls; + +device test; + +/// COMPILE-ONLY + +template t { + method m1() :{} + method m2() :default {} + method m3(); + + shared method sm1() :{} + shared method sm2() :default {} + shared method sm3(); +} + +template u { + method m1(); + method m2() :default {} + method m3(); +} + +template v is (t, u) { + method m2() {} + method m3() default {} + shared method sm2() {} + shared method sm3() default {} +} + +group g is v { + method m1(); + method m2(); + method m3(); + + method m3() {} + method sm3() {} +} diff --git a/test/1.4/provisional/T_explicit_method_decls_EOVERRIDEMETH.dml b/test/1.4/provisional/T_explicit_method_decls_EOVERRIDEMETH.dml new file mode 100644 index 00000000..ddb40e21 --- /dev/null +++ b/test/1.4/provisional/T_explicit_method_decls_EOVERRIDEMETH.dml @@ -0,0 +1,72 @@ +/* + © 2024 Intel Corporation + SPDX-License-Identifier: MPL-2.0 +*/ + +dml 1.4; + +/// ERROR ENOVERRIDEMETH +provisional explicit_method_decls; + +device test; + +/// ERROR ENOVERRIDEMETH +method m0() {} + +method m1a(); +// no error +method m1a() {} + +/// ERROR EOVERRIDEMETH +method m1b(); +/// ERROR EOVERRIDEMETH +method m1b() :{} + +method m2a(); +// no error +method m2a() default {} +/// ERROR EOVERRIDEMETH +method m2b() :default {} +/// ERROR EOVERRIDEMETH +method m2b(); + +template t { + method m3a() :default {} + method m3b() :default {} + method m4a() :default {} + /// ERROR EOVERRIDEMETH + method m4b() :default {} + /// ERROR ENOVERRIDEMETH + method m5() default {} + shared method m6a(); + /// ERROR EOVERRIDEMETH + shared method m6b(); + method m7a(); + /// ERROR EOVERRIDEMETH + method m7b(); + shared method m8a() :default {} + /// ERROR EOVERRIDEMETH + shared method m8b() :default {} + shared method m9() :{} + method m10() :{} +} + +group g is t { + method m3a() {} + /// ERROR EOVERRIDEMETH + method m3b() :{} + method m4a() default {} + /// ERROR EOVERRIDEMETH + method m4b() :default {} + method m6a() {} + /// ERROR EOVERRIDEMETH + method m6b() :{} + method m7a() {} + /// ERROR EOVERRIDEMETH + method m7b() :{} + method m8a() default {} + /// ERROR EOVERRIDEMETH + method m8b() :default {} + // no error + method m10(); +} diff --git a/test/1.4/provisional/T_explicit_param_decls_EOVERRIDE.dml b/test/1.4/provisional/T_explicit_param_decls_EOVERRIDEPARAM.dml similarity index 66% rename from test/1.4/provisional/T_explicit_param_decls_EOVERRIDE.dml rename to test/1.4/provisional/T_explicit_param_decls_EOVERRIDEPARAM.dml index 423c4f8a..77cf13ec 100644 --- a/test/1.4/provisional/T_explicit_param_decls_EOVERRIDE.dml +++ b/test/1.4/provisional/T_explicit_param_decls_EOVERRIDEPARAM.dml @@ -5,47 +5,47 @@ dml 1.4; -/// ERROR ENOVERRIDE +/// ERROR ENOVERRIDEPARAM provisional explicit_param_decls; device test; -/// ERROR ENOVERRIDE +/// ERROR ENOVERRIDEPARAM param p0 = 3; param p1a; // no error param p1a = 3; -/// ERROR EOVERRIDE +/// ERROR EOVERRIDEPARAM param p1b; -/// ERROR EOVERRIDE +/// ERROR EOVERRIDEPARAM param p1b := 3; param p2a; // no error param p2a default 3; -/// ERROR EOVERRIDE +/// ERROR EOVERRIDEPARAM param p2b :default 3; -/// ERROR EOVERRIDE +/// ERROR EOVERRIDEPARAM param p2b; template t { param p3a :default 3; param p3b :default 3; param p4a :default 4; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p4b :default 4; - /// ERROR ENOVERRIDE + /// ERROR ENOVERRIDEPARAM param p5 default 5; param p6a: int; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p6b: int; param p7a; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p7b; param p8a: int default 6; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p8b: int default 6; param p9: int = 7; param p10 := 10; @@ -53,19 +53,19 @@ template t { group g is t { param p3a = 33; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p3b := 33; param p4a default 44; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p4b :default 44; param p6a = 66; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p6b := 66; param p7a = 77; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p7b := 77; param p8a default 88; - /// ERROR EOVERRIDE + /// ERROR EOVERRIDEPARAM param p8b :default 88; // no error param p10;