Skip to content

Commit 7403342

Browse files
cknittcristianoc
andcommitted
Fold unboxed variant matches by runtime value
Adapt canonical constants and value-based dispatch from c32a837. Preserve safe folds while distinguishing arrays, the empty list, bigint spellings, and 32-bit integer tags. Replace the bailout tests with differential runtime coverage and value-based Lambda assertions. Co-authored-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent c1034e6 commit 7403342

11 files changed

Lines changed: 750 additions & 289 deletions

compiler/ml/lambda.ml

Lines changed: 134 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,14 @@ let const_constructor (tag : Variant_runtime.tag) =
422422
| Some (Variant_runtime.Int v) -> Const_int (Int32.of_int v)
423423
| _ -> Const_constructor tag
424424

425+
(* An untagged constructor has no runtime existence: [Color("primary")] is the
426+
string "primary", exactly as [Primary] is. Erasing the wrapper lets folding
427+
inspect the payload rather than a constructor the runtime cannot see. *)
428+
let const_block (tag_info : tag_info) (args : structured_constant list) =
429+
match (tag_info, args) with
430+
| Blk_constructor {runtime = {untagged = true}}, [payload] -> payload
431+
| _ -> Const_block (tag_info, args)
432+
425433
(* A constructor with an optional shape carries no payload when constant. *)
426434
let const_shape_none = Const_js_undefined {is_unit = false}
427435

@@ -773,6 +781,87 @@ and eq_option l1 l2 =
773781

774782
and eq_approx_list ls ls1 = Ext_list.for_all2_no_exn ls ls1 eq_approx
775783

784+
(* Classify constants by the JavaScript value emitted by [Lam_compile_const]
785+
and [Js_dump], not by their source constructor. Unknown representations
786+
must retain runtime dispatch. *)
787+
type value_kind =
788+
| Is_literal of Variant_runtime.literal_tag
789+
| Is_object
790+
| Is_array
791+
| Unknown_value
792+
793+
let rec runtime_value_kind (c : structured_constant) =
794+
match c with
795+
| Const_string s -> Is_literal (String s)
796+
| Const_int i -> Is_literal (Int (Int32.to_int i))
797+
| Const_float f -> Is_literal (Float f)
798+
| Const_js_true -> Is_literal (Bool true)
799+
| Const_js_false -> Is_literal (Bool false)
800+
| Const_js_null -> Is_literal Null
801+
| Const_js_undefined _ -> Is_literal Undefined
802+
| Const_polyvar name -> Is_literal (String name)
803+
| Const_constructor {name = "[]"; literal = None} -> Is_literal (Int 0)
804+
| Const_constructor {name; literal = None} -> Is_literal (String name)
805+
| Const_constructor {literal = Some (BigInt _)} -> Unknown_value
806+
| Const_constructor {literal = Some literal} -> Is_literal literal
807+
| Const_block (Blk_constructor {runtime = {untagged = true}}, args) -> (
808+
(* Also handle wrappers in constants read from existing compiler data. *)
809+
match args with
810+
| [payload] -> runtime_value_kind payload
811+
| _ -> Unknown_value)
812+
| Const_block (Blk_tuple, _) -> Is_array
813+
| Const_block (Blk_record {fields}, _) ->
814+
if
815+
Array.length fields <> 0
816+
&& Ext_array.for_alli fields (fun i (name, _) -> string_of_int i = name)
817+
then Is_array
818+
else Is_object
819+
| Const_block
820+
( ( Blk_constructor _ | Blk_record_inlined _ | Blk_poly_var
821+
| Blk_record_ext _ | Blk_module _ | Blk_module_export _ | Blk_extension
822+
),
823+
_ ) ->
824+
Is_object
825+
| Const_char _ | Const_bigint _ | Const_some _ | Const_module_alias
826+
| Const_assertfalse ->
827+
Unknown_value
828+
829+
(* Runtime equality, rather than equality of tags: [@as(1)] and a payload
830+
[1.0] are the same JavaScript number. Bigint spellings are not compared;
831+
both bigint payloads and bigint constructors are classified as unknown.
832+
Integer tags are emitted through [Int32.of_int], just like [Const_int]. *)
833+
let literal_denotes_same (a : Variant_runtime.literal_tag)
834+
(b : Variant_runtime.literal_tag) =
835+
match (a, b) with
836+
| String x, String y -> x = y
837+
| Int x, Int y -> Int32.of_int x = Int32.of_int y
838+
| Float x, Float y -> float_of_string x = float_of_string y
839+
| Int x, Float y | Float y, Int x ->
840+
Int32.to_float (Int32.of_int x) = float_of_string y
841+
| Bool x, Bool y -> x = y
842+
| Null, Null | Undefined, Undefined -> true
843+
| (String _ | Int _ | Float _ | Bool _ | BigInt _ | Null | Undefined), _ ->
844+
false
845+
846+
(* Mirror [Dynamic_checks]: literals take precedence; the object case excludes
847+
arrays when an array case exists. Int and float share one runtime type. *)
848+
let value_has_block_type ~block_types (kind : value_kind)
849+
(block_type : Variant_runtime.block_type) =
850+
match (block_type, kind) with
851+
| (IntType | FloatType), Is_literal (Int _ | Float _) -> true
852+
| StringType, Is_literal (String _) -> true
853+
| BooleanType, Is_literal (Bool _) -> true
854+
| ObjectType, Is_object -> true
855+
| InstanceType Array, Is_array -> true
856+
| ObjectType, Is_array ->
857+
not (List.mem (Variant_runtime.InstanceType Array) block_types)
858+
| UnknownType, (Is_literal _ | Is_object | Is_array) -> true
859+
| UnknownType, Unknown_value -> false
860+
| ( ( IntType | FloatType | StringType | BooleanType | ObjectType | BigintType
861+
| FunctionType | InstanceType _ ),
862+
_ ) ->
863+
false
864+
776865
let switch lam (lam_switch : lambda_switch) : t =
777866
let action_or_switch = function
778867
| Some action -> action
@@ -781,8 +870,49 @@ let switch lam (lam_switch : lambda_switch) : t =
781870
| Some action -> action
782871
| None -> Lswitch (lam, lam_switch))
783872
in
784-
match lam with
785-
| Lconst (Const_constructor cstr_name) ->
873+
(* An untagged variant is dispatched on the value, so a constant scrutinee is
874+
decided here rather than by the constructor it was written with - which
875+
has no runtime existence and may be shared with a literal constructor.
876+
[`Undecided] means this layer cannot name the constant's runtime shape, so
877+
the switch has to stay; it is not the same as "no case matches". *)
878+
let untagged_action (facts : Variant_runtime.matching_facts) cst =
879+
let find_in cases matches =
880+
`Case
881+
(Ext_list.find_opt cases (fun (key, action) ->
882+
match key with
883+
| Switch_constructor case when matches case -> Some action
884+
| Switch_int _ | Switch_constructor _ -> None))
885+
in
886+
let literal_of_tag (tag : Variant_runtime.tag) =
887+
match tag.literal with
888+
| Some literal -> literal
889+
| None -> Variant_runtime.String tag.name
890+
in
891+
let matches_block = value_has_block_type ~block_types:facts.block_types in
892+
let kind = runtime_value_kind cst in
893+
match kind with
894+
| Unknown_value -> `Undecided
895+
| Is_literal literal
896+
when Ext_list.exists facts.literal_tags (literal_denotes_same literal) ->
897+
(* The literal side wins, exactly as it does at runtime. *)
898+
find_in lam_switch.sw_consts (function
899+
| Constant tag -> literal_denotes_same literal (literal_of_tag tag)
900+
| Block _ -> false)
901+
| Is_literal _ | Is_object | Is_array ->
902+
(* Not a declared literal, so the payload's runtime shape decides. *)
903+
if Ext_list.exists facts.block_types (matches_block kind) then
904+
find_in lam_switch.sw_blocks (function
905+
| Block {block_type = Some block_type} ->
906+
matches_block kind block_type
907+
| Constant _ | Block {block_type = None} -> false)
908+
else `Undecided
909+
in
910+
match (lam, lam_switch.sw_dispatch) with
911+
| Lconst cst, Switch_variant ({block_types = _ :: _} as facts) -> (
912+
match untagged_action facts cst with
913+
| `Case action -> action_or_switch action
914+
| `Undecided -> Lswitch (lam, lam_switch))
915+
| Lconst (Const_constructor cstr_name), _ ->
786916
let action =
787917
Ext_list.find_opt lam_switch.sw_consts (fun (key, action) ->
788918
match key with
@@ -791,7 +921,7 @@ let switch lam (lam_switch : lambda_switch) : t =
791921
| Switch_int _ | Switch_constructor _ -> None)
792922
in
793923
action_or_switch action
794-
| Lconst (Const_int i) ->
924+
| Lconst (Const_int i), _ ->
795925
(* Because of inlining and dead code, we might be looking at a value of unexpected type
796926
e.g. an integer, so the const case might not be found *)
797927
let i = Int32.to_int i in
@@ -806,15 +936,7 @@ let switch lam (lam_switch : lambda_switch) : t =
806936
| Switch_int _ | Switch_constructor _ -> None)
807937
in
808938
action_or_switch action
809-
| Lconst
810-
(Const_block
811-
( ( Blk_constructor {runtime = {untagged = true}}
812-
| Blk_record_inlined {runtime = {untagged = true}} ),
813-
_ )) ->
814-
(* An untagged payload can have the same runtime value as a literal
815-
constructor. Its source constructor does not determine the match. *)
816-
Lswitch (lam, lam_switch)
817-
| Lconst (Const_block (tag_info, _)) ->
939+
| Lconst (Const_block (tag_info, _)), _ ->
818940
let runtime =
819941
match tag_info with
820942
| Blk_constructor {runtime} | Blk_record_inlined {runtime} -> Some runtime

compiler/ml/lambda.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,12 @@ val const_string : string -> structured_constant
432432
val const_of_typed : constant -> structured_constant
433433
val const_unit : structured_constant
434434
val const_constructor : Variant_runtime.tag -> structured_constant
435+
436+
val const_block : tag_info -> structured_constant list -> structured_constant
437+
(** Build a constant block, erasing the wrapper of an untagged constructor:
438+
its payload alone is the runtime value. Inline records remain blocks,
439+
since their fields form a runtime object. *)
440+
435441
val const_shape_none : structured_constant
436442
val const_polyvar : string -> structured_constant
437443
val const_polyvar_name : string -> structured_constant

compiler/ml/translcore.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.t =
11931193
runtime;
11941194
}
11951195
in
1196-
try const (Const_block (tag_info, List.map extract_constant ll))
1196+
try const (Lambda.const_block tag_info (List.map extract_constant ll))
11971197
with Not_constant ->
11981198
prim ~primitive:(Pmakeblock tag_info) ~args:ll e.exp_loc)
11991199
| Extension_constructor path ->

0 commit comments

Comments
 (0)