HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
3 stars 1 forks source link

Add support for `injectable` function parameters in DML #293

Closed EvanKirshenbaum closed 8 months ago

EvanKirshenbaum commented 8 months ago

I had intended that this would be implemented as part of adding support for general overloaded functions (#288), but that's turning out to be trickier than I had thought (partially due to COVID brain), and the OSU folks are going to want this sooner rather than later, so I think I'm going to try a somewhat kludgier approach for the short term.

When mocking up a macro for them, since there isn't support for general paths yet (#282), I found myself writing

define walk_circle(injectable drop, direction hdir)
{
   repeat for mix_time
   {
     drop : down 2
          : 4 in direction hdir
          : north 4
          : 4 in direction hdir turned around
          : south 2;
   }
}

The notion is that we have a two-argument function walk_circle(), but we want to be able to say

d : walk_circle(north);

By marking the drop parameter as injectable, when the compiler sees walk_circle(north), instead of complaining that an incorrect number of arguments have been supplied, it would remember the arguments passed in and return a new function that took one argument, spliced it into the correct position in the list, and then called the function.

I think that I should be able to do this pretty straightforwardly by adding the injectable positions to CallableType.

Note, that, unlike the approach I have been taking with #288, this would not result in walk_circle being treated as dir -> (drop -> no_val), although I guess I could probably make it work in CallableType._find_conversion_to().

Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 02, 2023 at 12:13 PM PDT. Closed on Aug 02, 2023 at 4:22 PM PDT.
EvanKirshenbaum commented 8 months ago

This issue was referenced by the following commit before migration:

EvanKirshenbaum commented 8 months ago

Okay, a bit of a kludge, but it appears to work, although it only supports a single injectable position.

CallableType now has

injectable_pos: Final[Optional[int]]
as_injection: Final[Optional[CallableType]]

injectable_pos tells the system where to put the delayed parameter, and as_injection is the resulting type signature.

In order to make this work, I also had to munge things so that as_injection's with_self_sig has the full type, not the resulting type. So, __init__() and find() take an optional as_self parameter. Note that this means that when computing as_injection, we don't call find(), because we don't want the munged type being returned in the future. I also changed the way the find() cache works. It now keys on both the Signature and the optional injectable position.

The actual value that's created is a new subclass of CallableValue that remembers the bound arguments, the injection point, and the full CallableValue:

lass BoundInjectionValue(CallableValue):
    bound_args: Final[tuple[Any, ...]]
    full_callable: Final[CallableValue]
    injection_pos: Final[int]

    def __init__(self, full_type: CallableType, full: CallableValue,
                 *args: Any) -> None:
        as_injection = not_None(full_type.as_injection)
        super().__init__(as_injection.sig)
        self.bound_args = args
        self.full_callable = full
        self.injection_pos = not_None(full_type.injectable_pos)

    def __str__(self) -> str:
        return f"({self.full_callable})({', '.join(str(a) for a in self.bound_args)})"

    def apply(self, args:Sequence[Any]) -> Delayed[Any]:
        assert len(args)==1
        full_args = list(self.bound_args)
        full_args.insert(self.injection_pos, args[0])
        return self.full_callable.apply(full_args)
Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 02, 2023 at 4:22 PM PDT.