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

Delay expansion errors for rules #3261

Merged
merged 3 commits into from
Mar 17, 2020
Merged

Conversation

rgrinberg
Copy link
Member

Previosuly, a failed expansion would immediately fail generating a rule.
Now, we delay the error using a [Build.fail].

I've left some inline comments to make it easy for reviewers.

let prefix_failures t b =
match t.failures with
| [] -> b
| fail :: _ -> Build.O.( >>> ) (Build.fail fail) b
Copy link
Member Author

Choose a reason for hiding this comment

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

This refactoring is useful to make sure that we use the same logic to prefix rules with the failure discovered in the expansion

@@ -322,11 +342,6 @@ let expand_and_record acc ~map_exe ~dep_kind ~expansion_kind
| Static -> fun _ -> User_error.raise ~loc [ cannot_be_used_here pform ]
| Dynamic -> Resolved_forms.add_ddep acc expansion
in
let add_fail =
match expansion_kind with
| Static -> fun _ (f : fail) -> f.fail ()
Copy link
Member Author

Choose a reason for hiding this comment

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

This clause was the cause of the error. It's too soon to raise the error at this stage, we should instead prefix the rule itself with the error.

Copy link

Choose a reason for hiding this comment

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

Makes sense

(fun () ->
let loc = String_with_vars.loc sw in
User_error.raise ~loc
[ Pp.text "failed to expand percent form" ])
Copy link
Member Author

Choose a reason for hiding this comment

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

The user should only see this error if they used some unknown percent form. Because the errors discovered before the expansion are prefixed.

@rgrinberg rgrinberg force-pushed the extra-deps branch 2 times, most recently from 62388f4 to 5c8f9a8 Compare March 14, 2020 16:28
Expander.Partial.expand_path expander s
|> String_with_vars.Partial.map ~f:(fun path ->
let+ () = Build.path path in
[ path ])
Copy link

Choose a reason for hiding this comment

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

I don't understand why we need to go through partial expansion here. The aim of partial expansion for actions is to be able to collect targets before the action is fully expanded. However, we never need this for the deps field given that it doesn't contain targets.

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to interpret deps before expanding targets. For example, consider how we evaluate something like this:

(action
 (deps (:foo %{bin:bar}))
 (target %{foo}))

Copy link

Choose a reason for hiding this comment

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

Agreed, but I still don't follow why we need to go through partial.

Copy link
Member Author

Choose a reason for hiding this comment

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

The errors are discovered as we're doing the expansion. When such an error is discovered, the expansion of that variable is aborted and hence the only way to recover is through a partial expansion.

I suppose it's possible to make this error some other exception. It could be caught and handled instead of handling partial expansions.

Copy link

Choose a reason for hiding this comment

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

Ok, I feel like I'll need to go over all this code again at some point. I'm finding it difficult to follow everything. When you have left the airport chaos, we can maybe go over this together via videoconf.

Though, if someone else can review this don't wait for me.

Copy link
Member Author

Choose a reason for hiding this comment

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

In case someone else is curious, here's the pseudo-code of the current code works:

String_with_vars.expand ~f:(fun var ->
  if binary_vailable var then
    add_failure resolved_forms;
    None (* This raises because we are in an [expand] *)
  else
    Some (binary_path)
)

My proposal is to change the above to use partial_expand over expand. This will prevent the None case from raising and we'll be able to extract the error from the resolved form.

To avoid partial expansion, I suggested changing add_failure resolved_forms to raise an exception. The caller can catch it and propagate it via Build.fail.

Copy link

Choose a reason for hiding this comment

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

Oh ok, I get it now!

In other places where we want to delay error reporting, such as for library name resolution, we are using the Or_exn.t monad. What do you think of doing the same here? That would make the intent explicit.

I was finding the code confusing because in my head partial is related to static vs dynamic. While here we are talking about delaying error reporting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I can do that. It would also make things a bit faster as we'd stop trying to expand the other variables.

Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
@rgrinberg
Copy link
Member Author

@diml should be ready now.

@@ -324,8 +351,9 @@ let expand_and_record acc ~map_exe ~dep_kind ~expansion_kind
in
let add_fail =
match expansion_kind with
| Static -> fun _ (f : fail) -> f.fail ()
| Dynamic -> Resolved_forms.add_fail
| Static -> Or_exn.raise
Copy link
Member Author

Choose a reason for hiding this comment

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

This wrapping is necessary so that the exception is caught by Expander.Or_error. We want to make sure that we don't accidentally delay exceptions that weren't raised by add_fail.

Copy link

Choose a reason for hiding this comment

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

I'm wondering if that's really necessary. I tried removing the wrapping and nothing broke in the testsuite. I'm also thinking that we could remove add_fail entirely and just add a catch all around expand_and_record. That would make the code simpler and easier to reason about. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm wondering if that's really necessary. I tried removing the wrapping and nothing broke in the testsuite.

Indeed. Another option is to just catch & delay User_error.E. I think we can reasonably assume that all errors can be delayed.

I'm also thinking that we could remove add_fail entirely and just add a catch all around expand_and_record. That would make the code simpler and easier to reason about. WDYT?

Seems fine to me. Just to confirm, you would then have to handle this expansion based on the expansion_kind. I.e.

...
with User_error.E _ as exn ->
  match exception_kind with
  | Dynamic -> Resolved_forms.add acc exn
  | Static -> reraise exn

Copy link

Choose a reason for hiding this comment

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

Yep. But I don't even think we need to match on User_error.E here. Catching all exceptions seems fine. We are just making the behaviour lazy, that's all.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it would be quite bad to swallow Code_error here. See the last commit, I've simplified the code further but I'm still not catching only User_errors. I think that's a safe compromise.

Copy link

Choose a reason for hiding this comment

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

Seems reasonable

| false ->
if Lib.DB.available (Scope.libs t.scope) lib then
User_error.E
(User_error.make ~loc
Copy link
Member Author

Choose a reason for hiding this comment

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

This error is now created more eagerly than necessary. I don't think this is worth worrying about however.

@ghost
Copy link

ghost commented Mar 17, 2020

I did a pass of refactoring and made the API of Expander a bit higher level. For instance, Resolved_forms is no longer exposed and is just an implementation detail. I feel like expander.ml could be simplified further, but that'll be for another time.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Looks good

@rgrinberg
Copy link
Member Author

@diml I removed the type variable from expander_result. Was it added by accident?

I agree this looks much better.

Previosuly, a failed expansion would immediately fail generating a rule.
Now, we delay the error using a [Build.fail].

Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
In expand_and_record, instead of receiving an argument describing how
the function should behave, we always do the same thing and add two
wrappers that decide what to do based on what behavior the caller
wanted.

Make [Resolved_forms] hidden inside [Expander]

Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
@rgrinberg rgrinberg merged commit 70c1dee into ocaml:master Mar 17, 2020
@ghost
Copy link

ghost commented Mar 18, 2020

@diml I removed the type variable from expander_result. Was it added by accident?

Indeed. It was a temporary state while I was rediscovering how all this works.

rgrinberg added a commit to rgrinberg/opam-repository that referenced this pull request Apr 10, 2020
…lugin, dune-private-libs and dune-glob (2.5.0)

CHANGES:

- Add a `--release` option meaning the same as `-p` but without the
  package filtering. This is useful for custom `dune` invocation in opam
  files where we don't want `-p` (ocaml/dune#3260, @diml)

- Fix a bug introduced in 2.4.0 causing `.bc` programs to be built
  with `-custom` by default (ocaml/dune#3269, fixes ocaml/dune#3262, @diml)

- Allow contexts to be defined with local switches in workspace files (ocaml/dune#3265,
  fix ocaml/dune#3264, @rgrinberg)

- Delay expansion errors until the rule is used to build something (ocaml/dune#3261, fix
  ocaml/dune#3252, @rgrinberg, @diml)

- [coq] Support for theory dependencies and compositional builds using
  new field `(theories ...)` (ocaml/dune#2053, @ejgallego, @rgrinberg)

- From now on, each version of a syntax extension must be explicitely tied to a
  minimum version of the dune language. Inconsistent versions in a
  `dune-project` will trigger a warning for version <=2.4 and an error for
  versions >2.4 of the dune language. (ocaml/dune#3270, fixes ocaml/dune#2957, @voodoos)

- [coq] Bump coq lang version to 0.2. New coq features presented this release
  require this version of the coq lang. (ocaml/dune#3283, @ejgallego)

- Prevent installation of public executables disabled using the `enabled_if` field.
  Installation will now simply skip such executables instead of raising an
  error. (ocaml/dune#3195, @voodoos)

- `dune upgrade` will now try to upgrade projects using versions <2.0 to version
  2.0 of the dune language. (ocaml/dune#3174, @voodoos)

- Add a `top` command to integrate dune with any toplevel, not just
  utop. It is meant to be used with the new `#use_output` directive of
  OCaml 4.11 (ocaml/dune#2952, @mbernat, @diml)

- Allow per-package `version` in generated `opam` files (ocaml/dune#3287, @toots)

- [coq] Introduce the `coq.extraction` stanza. It can be used to extract OCaml
  sources (ocaml/dune#3299, fixes ocaml/dune#2178, @rgrinberg)

- Load ppx rewriters in dune utop and add pps field to toplevel stanza. Ppx
  extensions will now be usable in the toplevel
  (ocaml/dune#3266, fixes ocaml/dune#346, @stephanieyou)

- Add a `(subdir ..)` stanza to allow evaluating stanzas in sub directories.
  (ocaml/dune#3268, @rgrinberg)

- Fix a bug preventing one from running inline tests in multiple modes
  (ocaml/dune#3352, @diml)

- Allow the use of the `%{profile}` variable in the `enabled_if` field of the
  library stanza. (ocaml/dune#3344, @mrmr1993)

- Allow the use of `%{ocaml_version}` variable in `enabled_if` field of the
  library stanza. (ocaml/dune#3339, @voodoos)

- Fix dune build freezing on MacOS when cache is enabled. (ocaml/dune#3249, fixes #ocaml/dune#2973,
  @artempyanykh)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants