Skip to content

[suggestion-ish] dynamic datatype method creation #420

Description

@SamsTheNerd

when making a datatype sort from a class, I couldn't find a way to programmatically create methods to get turned into constructors. My motivating use case is for integrating with a compiler where the built in types and operators are internally extensible defined with a registry+interface system. I figure I could just fall back to string or int labels with (op-wrapper op_name args) pattern but my (unsourced, entirely vibe based) guess would be that would be worse for performance? i'm coming from egg though so I don't have a strong intuition for query performance influences in egglog quite yet.

i have a hacked together solution that seems to work fine, but a neater solution probably exists. It can't just use a class decorator or otherwise just add the method via setattr since that doesn't seem to add it to the namespace dict that the expr metaclass looks through for methods.

@dataclass(frozen=True)
class DynamicMethodInfo:
    name: str
    # all types provided in annotations need to be Expr-convertable still.
    args_anns: list[tuple[str, str]] # (arg_name, arg_type) <- arg_type should be exactly as written in annotation. don't str(cls) it!
    # defaults to the type of the class implementing it.
    return_type: Optional[str] = None
    is_class_method: bool = True
    # args here should match the annotated args. return type should match up too. include cls up front if writing a classmethod
    default_impl: Optional[callable] = None

def DynamicExprMeta(method_provider: list[DynamicMethodInfo]) -> type:
    class _AddMethodsMeta(_ExprMetaclass):
        """Metaclass for dynamically adding methods to an egglog datatype class"""
        def __new__(
            cls: type[_AddMethodsMeta],
            name: str,
            bases: tuple[type, ...],
            namespace: dict[str, Any],
            egg_sort: str | None = None,
            ruleset: Ruleset | None = None,
        ):
            # print(method_provider)
            for mthd_info in method_provider:
                new_method = mthd_info.default_impl
                if new_method is None:
                    lambda_args = [arg for (arg, _) in mthd_info.args_anns]
                    if mthd_info.is_class_method:
                        lambda_args.insert(0, "cls")
                    new_method = eval(f'lambda {", ".join(lambda_args)}: None')
                if mthd_info.is_class_method:
                    new_method = classmethod(new_method)
                for (arg_lbl, arg_type) in mthd_info.args_anns:
                    new_method.__annotations__[arg_lbl] = arg_type
                new_method.__annotations__["return"] = mthd_info.return_type or name
                setattr(cls, mthd_info.name, new_method)
                # print(new_method.__annotations__)
                namespace[mthd_info.name] = new_method
            # print(namespace)
            return super().__new__(cls, name, bases, namespace, egg_sort, ruleset)
    return _AddMethodsMeta

class TestDynamicExpr(Expr, metaclass=DynamicExprMeta([DynamicMethodInfo("test", [("arg1", "i64Like")])])):
    pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions