Skip to content

preemptively infer var_info annotations#49

Merged
dumbbell merged 9 commits into
rabbitmq:mainfrom
the-mikedavis:md-preemptive-var-info-inference
Feb 15, 2022
Merged

preemptively infer var_info annotations#49
dumbbell merged 9 commits into
rabbitmq:mainfrom
the-mikedavis:md-preemptive-var-info-inference

Conversation

@the-mikedavis

Copy link
Copy Markdown
Collaborator

supersedes #45
closes #44

This PR:

  • moves the annotation-guessing for tuples and maps into pass1_process_instructions/1
  • guesses accepts_match_context annotations for bs_start_match* instructions
  • refactors the add_comment_* functionality to add annotations at exact indices within instruction lists upon compile:forms/2 failure

This is still WIP while I figure out a good way to handle the hard-coded register 🤔

Given some code like the following:

    trim_leading_dash1(<<$-, Rest/binary>>) -> trim_leading_dash1(Rest);
    trim_leading_dash1(Binary)              -> Binary.

The compiler will add an annotation for the {x,0} register

    {'%',{var_info,{x,0},[accepts_match_context]}}.

This annotation is not available in the disasm, so when we recompile
this function, the compiler gives an error.

This change detects which x-register should accept the match context
by peeking the first instruction after the entrypoint label. This
instruction can either be a {test,bs_start_match3,..} instruction or
bs_start_match4 (if the compiler can determine that the function will
_always_ be passed a binary), both of which contain the register
which will be matched.

closes rabbitmq#44
see also rabbitmq#36
This separates out the function clauses for handling failures from
beam_validator, which allows us to write clauses for validator
failures without duplication (because of the different shape of
compiler errors between Erlang 23 and 24). This commit also fixes
the prior commit's compatibility with Erlang 23.
I don't have a good test case for this but it should be possible
to have this fail when the function does more than just call
another function and then return.
In many cases we can infer the necessary typing of a variable based on
an instruction in a function body. For example, when we see

    {get_tuple_element, Src, Element, _Dst}

We know that the register Src must contain a tuple with at least
`Element + 1' elements (if it weren't, the code could not have been
initially compiled), so we can add

    {'%', {var_info, Src, [{type, {t_tuple, Element + 1, false, #{}}}]}}

To satisfy the beam_validator's type-checks. This commit removes the clause
of `handle_validation_error/3' which would correct these failures when
`compile:forms/2' would emit them: that clause no longer matches.
Comment thread src/khepri_fun.erl
Comment thread src/khepri_fun.erl
Comment on lines +305 to +310
{{Call, 1 = _Arity, {f, EntryLabel}},
_Index,
no_bs_start_match2}},
_Error) when Call =:= call orelse Call =:= call_only ->
%% TODO: hard-coded register
Comment = {'%', {var_info, {x, 0}, [accepts_match_context]}},

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently hard-coded to work for a 1-arity function so that we know which register to annotate. I think to know which register to use we may need to track the typing of functions which might be difficult. I'll think about it some more 🤔

parse_float/1 starts with a bs_start_match4 instruction that needs
to both accept match contexts and must be typed as a t_bitstring in
order to satisfy the beam_validator. t_bitstring cannot be inferred
in pass1_process_instructions/3 (see the parent commit), so it needs
to be handled after it is noticed in the error of compile:forms/2.
See the child commit for the necessary changes to compile
parse_float/1 in a khepri_fun.
@the-mikedavis
the-mikedavis force-pushed the md-preemptive-var-info-inference branch from d8b06f1 to 86bf6fe Compare February 9, 2022 19:53
@coveralls

coveralls commented Feb 9, 2022

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 355

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 48 of 55 (87.27%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.4%) to 85.633%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/khepri_fun.erl 48 55 87.27%
Totals Coverage Status
Change from base Build 348: 0.4%
Covered Lines: 1514
Relevant Lines: 1768

💛 - Coveralls

@the-mikedavis

the-mikedavis commented Feb 9, 2022

Copy link
Copy Markdown
Collaborator Author

It seems like a lot of work to try to reconstruct all of the annotations beam_validator needs and I think these annotations end up only being interpreted by beam_validator. Maybe we should propose adding an option to compile:forms/2 to disable the beam_validator pass?

edit: the change would be very small (dcaefd9) but since this wouldn't be usable unti it's merged, I suspect we'll still need to handle beam_validator failures here for backwards compatibility, right?

@dumbbell

Copy link
Copy Markdown
Collaborator

It seems like a lot of work to try to reconstruct all of the annotations beam_validator needs and I think these annotations end up only being interpreted by beam_validator. Maybe we should propose adding an option to compile:forms/2 to disable the beam_validator pass?

edit: the change would be very small (dcaefd9) but since this wouldn't be usable unti it's merged, I suspect we'll still need to handle beam_validator failures here for backwards compatibility, right?

Right. But even with this option, I think I prefer to get those issues and validation errors to make sure the code is correct. More importantly, if an instruction such as get_tuple_element is called with something else than a tuple, the VM terminates with a segfault. These assertions (and the segfault) helped me discover bugs and I think it's best to understand the problems and emit a valid assembly form.

@dumbbell dumbbell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this! I submitted more inline comments.

Comment thread src/khepri_fun.erl Outdated
Comment thread src/khepri_fun.erl Outdated
Comment thread src/khepri_fun.erl
@the-mikedavis

the-mikedavis commented Feb 10, 2022

Copy link
Copy Markdown
Collaborator Author

(I'll squash the 'fix variable name...' commit before taking this out of draft status)

edit: squashed! ✔️

Comment thread src/khepri_fun.erl Outdated
Comment thread src/khepri_fun.erl Outdated
Comment thread src/khepri_fun.erl
Comment thread src/khepri_fun.erl Outdated
The implementation of add_comment_to_code/5 prior to this commit
placed comments into function code immediately following the function's
entry-point label, which is correct in most scenarios. Some validation
failures may need comments at other points in the function code, though.
Luckily, beam_validator points to the location of the failing instruction
in the failure triple. So this commit allows one to pass the location where
the comment should go end up in the function code.

Included with this new structure are two validation-failure handling clauses:
one may add type information to a bs_start_match4 instruction when the
validator is not satisfied by the current lack of typing. (I don't believe
you can infer the typing of a bs_start_match4 easily, we'd have to look
into the validator to see how it's detecting the 'needed' type.) The other
is an interesting case where an intermediary function needs an
'accepts_match_context' annotation. In this case, function A calls function
B with some variable that accepts a match context. Function B then calls
function C, which accepts a variable that must be a match context. Functions
A and C may have their variables properly annotated, but function B does not,
because it does not contain any bs_start_match* instructions. It may look
something like:

    {function, 'B', 1, 30,
      [
        {label, 29},
        {func_info, {atom, my_module}, {atom, 'B'}, 1},
        {label, 30},
        {call_only, 1, {f, 32}} %% function 'C'
      ]}

So in this case we ignore the FailingFun and instead look at the entry-label
in the failing call/call_only label and use that to find the function. Then
we annotate the {x,0} variable in that function to accept a match context,
which satisfies the validator.

This is currently hard-coded for a 1-arity function. More testing will be
needed to find an example of this where we can't blindly assume {x,0}.
add_comment_to_code/4 now takes Location as a pair

    {Placement :: before | 'after', Instruction :: tuple() | atom()}

And will use the instruction as the insertion point, placing the
annotation before or after it. This resolves some awkwardness from
using Location as an (1-indexed) index in the list of instructions of
a function. The instruction index works but is brittle when hard-coded
as it was for the `no_bs_start_match2' instruction. For example if
line instructions were not discarded, it could be impossible to tell
where in the instruction list the annotation should go.
Here we peek at the next instruction in either Result or Rest
(depending on Placement) to find an existing comment if there is
one and then we discard the existing comment after merging.
`beam_validator' currently accepts duplicate annotations for a
types: it discards all but the last annotation before a variable
is checked. For example if we were fixing the typing of a
`bs_start_match4`, we might end up with:

    {'%', {var_info, {x, 0}, [accepts_match_context]}},
    {'%', {var_info, {x, 0}, [{type, {t_bs_context,1,0,0}},
                              accepts_match_context]}},
    {bs_start_match4,{atom,resume},2,{x,0},{x,0}}.

Currently `beam_validator` behaves as though it discards the first
annotation. This behavior is not documented, though, so we protect
against a future where `beam_validator' stops handling duplicates
like this by ourselves discarding the existing annotation.
Here we avoid an infinite loop of compilation that can happen
when we attempt to fix a compilation error by adding an annotation
and that annotation already exists where we expect. This catches the
case where we expect an annotation to fix a compilation error, we
add the annotation, and then the compiler fails with the same error.
@the-mikedavis
the-mikedavis force-pushed the md-preemptive-var-info-inference branch from b9f5b0b to 2403ae6 Compare February 14, 2022 15:05
@the-mikedavis

Copy link
Copy Markdown
Collaborator Author

I think it makes sense to tackle the 'TODO' for the hard-coded register (this) in a separate PR: it will probably be a decently large change on its own. What do you think?

@the-mikedavis
the-mikedavis marked this pull request as ready for review February 14, 2022 15:07
@dumbbell

Copy link
Copy Markdown
Collaborator

I think it makes sense to tackle the 'TODO' for the hard-coded register (this) in a separate PR: it will probably be a decently large change on its own. What do you think?

Yes, I agree, let's see how it goes with this patch already.

Again, thank you for this great improvement!

@dumbbell
dumbbell merged commit 4dfd91d into rabbitmq:main Feb 15, 2022
@the-mikedavis
the-mikedavis deleted the md-preemptive-var-info-inference branch February 15, 2022 13:33
@dumbbell dumbbell added the enhancement New feature or request label Mar 23, 2022
@dumbbell dumbbell self-assigned this Mar 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

guess the accepts_match_context var_info annotation upon no_bs_start_match2 failure

3 participants