-
Notifications
You must be signed in to change notification settings - Fork 453
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
Use RescriptError for exceptions #6979
base: master
Are you sure you want to change the base?
Use RescriptError for exceptions #6979
Conversation
ed0627e
to
574b53b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cknitt Letting you know that tests fail with the following error. I know there's an unresolved issued with imports in the PR, so I don't even expect them to pass. But the error is not useful at all:
let internalFromExtension = (_ext: 'a): exn => { | ||
%raw(`Object.assign(new RescriptError(_ext.RE_EXN_ID), _ext)`) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it can be optimised to store the whole payload in an error field instead of spreading the object. But it requires modifying the matching logic, so I've done the easiest thing here to have at least something working
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to finish the PR with the implementation and improve it in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we just new RescriptError(id, payload)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll still require to do Object.assign
inside of the RescriptError
constructor. Until I updated matching logic, it's not possible to get rid of Object.assign
: J.expression = | ||
let field_name = | ||
match ext with | ||
| Blk_extension -> ( | ||
| Blk_extension _ -> ( | ||
fun i -> | ||
match i with 0 -> Literals.exception_id | i -> "_" ^ string_of_int i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change should make extension fields have the same logic as variants. Currently if an extension has a payload, it starts with _1
while normal variants start with _0
. I think it makes sense to have it unified. Should I include the change in a separate PR?
match i with 0 -> Literals.exception_id | i -> "_" ^ string_of_int i) | |
match i with 0 -> Literals.exception_id | i -> "_" ^ string_of_int (i - 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unified sounds good to me. If this causes a lot of output changes, then yes, maybe better in a separate PR.
It'll be better to completely remove the dependency on It has its own runtime that tracks exn symbols. It can be easily replaced with JavaScript native symbols. -let A = Caml_exceptions.create("A");
+let A = Symbol.for("A"); |
I think it's a good idea, I can probably do it in another PR. But probably it should be |
|
Isn't it what we need? |
I mentioned it because I wanted to drop the |
We can use But if we use unique symbols in runtime, we need another protocol (import/export) for it. |
The created exception identifiers are already exported, so the same mechanism should work with symbols |
Ok, will have a look at that error later. |
It's not the complete implementation, but it works |
@cknitt @cristianoc ready for review |
: J.expression = | ||
let field_name = | ||
match ext with | ||
| Blk_extension -> ( | ||
| Blk_extension _ -> ( | ||
fun i -> | ||
match i with 0 -> Literals.exception_id | i -> "_" ^ string_of_int i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unified sounds good to me. If this causes a lot of output changes, then yes, maybe better in a separate PR.
} else { | ||
JsError(e) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what we throw if we are rethrowing a non-ReScript exception, right?
So in this case we are throwing something that's not an instance of Error
/RescriptError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, it's a mistake
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I wanted to investigate a special case for JsError where it's not wrapped in RescriptError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
311ba6e
to
e10a8c0
Compare
@DZakh out of curiosity, does this change anything for the end-users? |
If they don't do dirty things with runtime representation of exceptions, then it should stay the same for them. |
Would be good to have it in the log I think. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving some comments.
I'd like to see a detailed review from @cometkim
@@ -3674,7 +3674,7 @@ let rec subtype_rec env trace t1 t2 cstrs = | |||
true (* handled in the fields checks *) | |||
| Record_unboxed b1, Record_unboxed b2 -> b1 = b2 | |||
| Record_inlined _, Record_inlined _ -> repr1 = repr2 | |||
| Record_extension, Record_extension -> true | |||
| Record_extension b1, Record_extension b2 -> b1.is_exception = b2.is_exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this casting any exception to any other exception?
OK seems legit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually don't know what the code is about, I just made it compile in the most safe way :)
@@ -2918,7 +2918,9 @@ let partial_function loc () = | |||
let fname = | |||
Filename.basename fname | |||
in | |||
Lprim(Praise Raise_regular, [Lprim(Pmakeblock(Blk_extension), | |||
Lprim(Praise Raise_regular, [Lprim(Pmakeblock(Blk_extension { | |||
is_exception = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you know that there's no other place in the compiler where one could have forgotten to do this?
Are there enough tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E.g. let e = E(10)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you talking about adding the correct is_exception
value? There is compiler complaining in every place where it's needed + there's record_extension_test.js
and multiple other tests showing that output is correct.
@@ -282,15 +283,15 @@ and constructor_tag = | |||
Cstr_constant of int (* Constant constructor (an int) *) | |||
| Cstr_block of int (* Regular constructor (a block) *) | |||
| Cstr_unboxed (* Constructor of an unboxed type *) | |||
| Cstr_extension of Path.t (* Extension constructor *) | |||
| Cstr_extension of Path.t * bool (* Extension constructor. true if is_exception *) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might break binary compatibility.
Need to check.
CC @zth
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this too.
We could modify ml types to avoid major complexity, but wouldn't that make potential compatibility issues? At least until we have a first-class toolchain for building PPXs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Report: Impact of Proposed Change in Runtime Representation
High-Level Summary
The proposed change in runtime representation introduces significant structural differences that can lead to severe incompatibilities with existing code. Specifically, when casting old data types to the new representation using Obj.magic
, the program is prone to crashes due to mismatches in memory layout. This highlights the need for careful handling and testing when evolving data structures to ensure backward compatibility and avoid runtime errors.
Test Results
-
New Value with Old Code: The program successfully processed a value created with the new type using the old code. The output was correct, and no crashes occurred. This indicates that the old code is capable of handling the new structure, at least when it ignores additional fields that were introduced in the new representation.
-
Old Value with New Code: The program crashed with a segmentation fault when attempting to process a value created with the old type using the new code. The crash occurred because the new code attempted to access fields that do not exist in the old memory layout, demonstrating the risk of using the new representation with existing data.
Supporting Code
The following code was used to test the impact of the proposed runtime representation change. It illustrates the potential issues that arise when interacting with old and new data structures:
module Path = struct
type t = string list
end
(* Old variant type definition *)
type my_variant_old =
| Old_extension of Path.t
(* Define a more complex structure for the new variant *)
type additional_info = {
id: int;
description: string;
}
(* New variant type definition *)
type my_variant_new =
| New_extension of Path.t * additional_info
(* Function that operates on the old type definition *)
let process_variant_old = function
| Old_extension path ->
Printf.printf "Inside process_variant_old\n%!";
let length = List.length path in
Printf.printf "Length: %d\n%!" length;
length
(* Function that operates on the new type definition *)
let process_variant_new = function
| New_extension (path, info) ->
Printf.printf "Inside process_variant_new\n%!";
Printf.printf "ID: %d\n%!" info.id;
Printf.printf "Description: %s\n%!" info.description;
Printf.printf "Length: %d\n%!" (List.length path)
(* Function to test a new value with the old code *)
let test_new_value_with_old_code () =
Printf.printf "Testing new value with old code:\n%!";
let new_value = New_extension (["a"; "b"; "c"], { id = 42; description = "Test" }) in
Printf.printf "Created new value\n%!";
let casted_value = Obj.magic new_value in
Printf.printf "After casting to old value\n%!";
process_variant_old casted_value
(* Function to test an old value with the new code *)
let test_old_value_with_new_code () =
Printf.printf "Testing old value with new code:\n%!";
let old_value = Old_extension ["a"; "b"; "c"] in
Printf.printf "Created old value\n%!";
let casted_value = Obj.magic old_value in
Printf.printf "After casting to new value\n%!";
process_variant_new casted_value
(* Main function to run the tests *)
let () =
Printf.printf "Running tests...\n%!";
test_new_value_with_old_code ();
Printf.printf "Finished testing new value with old code\n%!";
test_old_value_with_new_code ();
Printf.printf "Finished testing old value with new code\n%!"
This code confirms that the proposed changes to the runtime representation work without issue when the new structure is used with old code but cause a crash when old data is processed by the new code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zth this restrict the way the editor extension can evolve -- what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the current arg is changed to an inline record instead? Would that preserve the memory layout enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So overall what this situation is saying is:
1 make this change and all is fine
2 update the vendored .ml files in the vscode extension and run it on older version of the compiler: recipe for trouble
One possible solution is:
1.5 the extension moves inside the compiler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One possible solution is:
1.5 the extension moves inside the compiler
Created #6994.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cristianoc Currently working on moving the extension into the compiler.
Can this PR be merged already, or do you want to wait until the move is complete?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine by me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then I'll resolve conflicts, when I have time
| Cstr_extension (path1), Cstr_extension (path2) -> | ||
Path.same path1 path2 | ||
| Cstr_extension (path1, is_exception1), Cstr_extension (path2, is_exception2) -> | ||
Path.same path1 path2 && is_exception1 = is_exception2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is good. Not a suggestion to change.
Just an additional curiosity: if the path is the same, is it possible that the is_exception values don't agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If path
includes the module, then checking only path value should be enough.
if (e == null) { | ||
return false; | ||
function is_extension(any) { | ||
if (any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be made more precise? Should it be not null
, not undefined
, both?
Perhaps it's safe anyway. Just double checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's safe, but I wonder why &&
was generated to if/else
throw new Error(exn.RE_EXN_ID, { | ||
cause: exn | ||
}); | ||
throw exn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that this PR respects existing behavior. So this is just note for the future change
throw exn; | |
throw raw_exn; |
For example, suppose an exception raised in JS goes through a utility written in ReScript and then reaches JS again.
ReScript wraps all thrown values in RescriptError, regardless of whether it matches or not.
It can be problematic for FFIs because it can harm the intent of the external source.
Here's the ideal output I can think of:
let A = Symbol.for("rescript.exn$Module.A")
try {
throw new RescriptError(A, payload);
} catch (raw) {
if (RescriptError.isExn(raw, A)) {
// ...just match, no implicit conversion
}
// rethrow value as-is
throw raw;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally had the issue in rescript-schema. Had to solve with a hacky workaround https://github.com/DZakh/rescript-schema/blob/617ba9f38e1f06e0f4f98be029b78bcaa10898c7/src/S_Core.res#L413-L419
e10a8c0
to
66f833b
Compare
Could you undo the CHANGELOG formatting changes? |
66f833b
to
1b392d9
Compare
@zth wrote in #6984 (comment), referring to this PR:
So @DZakh could you rebase so that we can get this merged (and I guess you need to include the revert of the revert from #7016 first)? |
I'll do after the PR is merged #6984 |
deadlock 😄 #6984 (comment) |
No description provided.