Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fall bad generated functions back to interpreter #50348

Merged
merged 2 commits into from
Jun 29, 2023
Merged

Fall bad generated functions back to interpreter #50348

merged 2 commits into from
Jun 29, 2023

Commits on Jun 29, 2023

  1. Fall bad generated functions back to interpreter

    This fixes #49715. The fix itself is pretty simple - just remove
    the generator expansion that was added in #48766, but the bigger
    question here is what the correct behavior should be in the first place.
    
    # Dynamic Semantics, generally
    
    The primary question here are of the semantics of generated functions.
    Note that this is quite different to how they are implemented. In
    general, the way we think about compiling Julia is that there is a
    well defined set of *dynamic semantics* that specify what a particular
    piece of Julia code means. Julia's dynamic semantics are generally quite
    simple (at every point, call the most specific applicable method).
    What happens under the hood may be quite different (e.g. lots of inference,
    compiling constant folding, etc), but the compilation process should
    mostly preserve the semantics (with a few well defined exceptions
    around floating point arithmetic, effect assumptions, semantically
    unobservable side effects, etc.).
    
    # The dnymaic semantics of generated functions
    
    With that diatribe out of the way, let's think about the dynamic semantics
    of generated functions. We haven't always been particularly clear about
    this, but I propose it's basically the following:
    
    For a generated function:
    ```
    @generated function f(args...)
        # = generator body =#
    end
    ```
    
    this is semantically equivalent to the function to basically the following:
    
    ```
    const lno = LineNumberNode(@__FILE__, @__LINE__); function f(args...)
        generator = @opaque @assume_effects :foldable :generator (args...)->#= generator body =#
        body = generator(Base.get_world_counter(), lno, Core.Typeof.(args))
        execute(body, f, args...)
    end
    ```
    
    A couple of notes on this:
    
    1. `@opaque` used here for the world-age capture semantics of the generator itself
    2. There's an effects-assumption `:generator` that doesn't exist but is
       supposed to capture the special allowance for calling generators. This
       is discussed more below.
    
    ## Implementing `execute`
    
    For a long time, we didn't really have a first-class implementation of `execute`.
    It's almost (some liberties around the way that the arguments work, but you get
    the idea)
    
    ```
    execute_eval(body, f, args...) = eval((args...)->$body)(f, args....)
    ```
    
    but that doesn't have the correct world age semantics (would error
    as written and even if you used invokelatest, the body would run
    in the wrong world).
    
    However, with OpaqueClosure we do actually have a mechanism now and
    we could write:
    
    ```
    execute(body, f, args...) = OpaqueClosure(body, f)(args...)
    ```
    
    Again, I'm not proposing this as an implementation, just to give us an idea
    of what the dynamic semantics of generated functions are.
    
    # The particular bug (#49715)
    
    The issue in #49715 is that the following happens:
    1. A generated function gets called and inference is attempted.
    2. Inference attempts to infer the generated function and call the generator.
    3. The generator throws an error.
    4. Inference fails.
    5. The compiler enters a generic inference-failure fallback path
    6. The compiler asks for a generator expansion in the generic world (-1)
    7. This gives a different error, confusing the user.
    
    There is the additional problem that this error gets thrown at
    compilation time, which is not technically legal (and there was an
    existing TODO to fix that).
    
    In addition to that, I think there is a separate question of whether it
    should be semantically legal to throw an error for a different world age
    than the currently running one. Given the semantics proposed above, I
    would suggest that the answer should be no. This does depend on the
    exact semantics of :generator, but in general, our existing
    effects-related notions do not allow particularly strong assumptions on
    the particular error being thrown (requiring them to be re-evaluated
    at runtime), and I see no reason to depart from this practice here.
    
    Thus, I would suggest that the current behavior should be disallowed
    and the expected behavior is that the generic fallback implementation
    of generated functions invoke the generator in the runtime world and
    expose the appropriate error.
    
    # Should we keep the generic world?
    
    That does leave the question what to do about the generic world (-1).
    I'm not 100% convinced that this is necessarily a useful concept to
    have. It is true that most generated functions do not depend on the
    world age, but they can already indicate this by returning a value
    with bounded world range and no backedges (equivalently returning
    a plain expression). On the other hand, keeping the generic world
    does risk creating the inverse of the situation that prompted this
    issue, in that there is no semantically reachable path to calling
    the generator with the generic world, making it hard to debug.
    
    As a result, I am very strongly leaning towards removing this concept,
    but I am open to being convinced otherwise.
    
    # This PR
    
    This PR, which is considerably shorter than this commit message is very
    simple: The attempt to invoke the generator with the generic world -1
    is removed. Instead, we fall back to the interpreter, which already
    has the precise semantics that I want here - invoking the generator
    in the dynamic world and interpreting the result.
    
    # The semantics of :generator
    
    That leaves one issue to be resolved which is the semantics of `:generator`.
    I don't think it's necessary to be as precise here as we are about the
    other effects we expose, but I propose it be something like the following:
    
    For functions with the :generator effects assumption, :consistent-cy is
    relaxed as follows:
    
    1. The requistive notion of equality is relaxed to a "same code and
       metadata" equality of code instances. I don't think we have any
       predicate for this (and it's not necessarily computable), but the
       idea should be that the CodeInstance is always computed in the exact
       same way, but may be mutable and such. Note that this is explicitly
       not functional extensionality, because we do analyze the structure of
       the returned code and codegen based on it.
    
    2. The world-age semantics of :consistent sharpened to require
       our relaxed notion of consistency for any overlapping min_world:max_world
       range returned from the generator.
    Keno committed Jun 29, 2023
    Configuration menu
    Copy the full SHA
    4a98632 View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    bf69df0 View commit details
    Browse the repository at this point in the history