diff --git a/docs/internals/builtins/array/array_all.md b/docs/internals/builtins/array/array_all.md index 4cfd89ff74..1a33cc5e6c 100644 --- a/docs/internals/builtins/array/array_all.md +++ b/docs/internals/builtins/array/array_all.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_all.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1536](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1536) (`lower_array_all`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1707](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1707) (`lower_array_all`) - **Function symbol**: `lower_array_all()` diff --git a/docs/internals/builtins/array/array_any.md b/docs/internals/builtins/array/array_any.md index ea94d82d84..51d3e26676 100644 --- a/docs/internals/builtins/array/array_any.md +++ b/docs/internals/builtins/array/array_any.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_any.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_any.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1531](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1531) (`lower_array_any`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1702](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1702) (`lower_array_any`) - **Function symbol**: `lower_array_any()` diff --git a/docs/internals/builtins/array/array_chunk.md b/docs/internals/builtins/array/array_chunk.md index 260273a8ea..ac5dc25623 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_chunk.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:81](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L81) (`lower_array_chunk`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:87](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L87) (`lower_array_chunk`) - **Function symbol**: `lower_array_chunk()` diff --git a/docs/internals/builtins/array/array_combine.md b/docs/internals/builtins/array/array_combine.md index 0ae8336bd0..7ce78dcd79 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_combine.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:152](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L152) (`lower_array_combine`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:158](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L158) (`lower_array_combine`) - **Function symbol**: `lower_array_combine()` diff --git a/docs/internals/builtins/array/array_diff.md b/docs/internals/builtins/array/array_diff.md index 93df2048fe..de71bb0dd3 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:874](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L874) (`lower_array_diff`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:996](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L996) (`lower_array_diff`) - **Function symbol**: `lower_array_diff()` @@ -31,7 +31,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_diff(array $array, ...$arrays): array +function array_diff(array $array, ...$arrays): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_diff_assoc.md b/docs/internals/builtins/array/array_diff_assoc.md index ec0d08e3d9..fff1921d74 100644 --- a/docs/internals/builtins/array/array_diff_assoc.md +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_assoc.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1347](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1347) (`lower_array_diff_assoc`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1518](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1518) (`lower_array_diff_assoc`) - **Function symbol**: `lower_array_diff_assoc()` diff --git a/docs/internals/builtins/array/array_diff_key.md b/docs/internals/builtins/array/array_diff_key.md index 79b5808c7b..2b09006c5a 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_key.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:896](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L896) (`lower_array_diff_key`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1018](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1018) (`lower_array_diff_key`) - **Function symbol**: `lower_array_diff_key()` diff --git a/docs/internals/builtins/array/array_fill.md b/docs/internals/builtins/array/array_fill.md index d1db63df9e..7554e6da29 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:115](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L115) (`lower_array_fill`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:121](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L121) (`lower_array_fill`) - **Function symbol**: `lower_array_fill()` diff --git a/docs/internals/builtins/array/array_fill_keys.md b/docs/internals/builtins/array/array_fill_keys.md index 6f292d3c3b..0589241ab9 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill_keys.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:138](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L138) (`lower_array_fill_keys`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:144](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L144) (`lower_array_fill_keys`) - **Function symbol**: `lower_array_fill_keys()` diff --git a/docs/internals/builtins/array/array_filter.md b/docs/internals/builtins/array/array_filter.md index 26274ba7a3..4af174836d 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_filter.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:211](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L211) (`lower_array_filter`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:217](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L217) (`lower_array_filter`) - **Function symbol**: `lower_array_filter()` diff --git a/docs/internals/builtins/array/array_find.md b/docs/internals/builtins/array/array_find.md index 5f0f7618e7..b18d7e69a7 100644 --- a/docs/internals/builtins/array/array_find.md +++ b/docs/internals/builtins/array/array_find.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_find.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_find.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1526](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1526) (`lower_array_find`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1697](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1697) (`lower_array_find`) - **Function symbol**: `lower_array_find()` diff --git a/docs/internals/builtins/array/array_flip.md b/docs/internals/builtins/array/array_flip.md index 000e5a86a9..d93f7d7006 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_flip.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:171](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L171) (`lower_array_flip`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:177](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L177) (`lower_array_flip`) - **Function symbol**: `lower_array_flip()` diff --git a/docs/internals/builtins/array/array_intersect.md b/docs/internals/builtins/array/array_intersect.md index 02adb22a72..2b930e4558 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:885](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L885) (`lower_array_intersect`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1007](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1007) (`lower_array_intersect`) - **Function symbol**: `lower_array_intersect()` @@ -29,7 +29,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_intersect(array $array, ...$arrays): array +function array_intersect(array $array, ...$arrays): mixed ``` ## What the type checker enforces diff --git a/docs/internals/builtins/array/array_intersect_assoc.md b/docs/internals/builtins/array/array_intersect_assoc.md index 03a7fab958..3f9cc28251 100644 --- a/docs/internals/builtins/array/array_intersect_assoc.md +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_assoc.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1352](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1352) (`lower_array_intersect_assoc`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1523](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1523) (`lower_array_intersect_assoc`) - **Function symbol**: `lower_array_intersect_assoc()` diff --git a/docs/internals/builtins/array/array_intersect_key.md b/docs/internals/builtins/array/array_intersect_key.md index 3456d6e1d9..564f06b771 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_key.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:901](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L901) (`lower_array_intersect_key`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1023](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1023) (`lower_array_intersect_key`) - **Function symbol**: `lower_array_intersect_key()` diff --git a/docs/internals/builtins/array/array_is_list.md b/docs/internals/builtins/array/array_is_list.md index 8b0567868f..940a611766 100644 --- a/docs/internals/builtins/array/array_is_list.md +++ b/docs/internals/builtins/array/array_is_list.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_is_list.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_is_list.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1144](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1144) (`lower_array_is_list`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1315](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1315) (`lower_array_is_list`) - **Function symbol**: `lower_array_is_list()` diff --git a/docs/internals/builtins/array/array_key_first.md b/docs/internals/builtins/array/array_key_first.md index ccc71fdcfd..b242430db8 100644 --- a/docs/internals/builtins/array/array_key_first.md +++ b/docs/internals/builtins/array/array_key_first.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_first.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_first.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1155](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1155) (`lower_array_key_first`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1326](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1326) (`lower_array_key_first`) - **Function symbol**: `lower_array_key_first()` diff --git a/docs/internals/builtins/array/array_key_last.md b/docs/internals/builtins/array/array_key_last.md index 627d0ede5d..650187b318 100644 --- a/docs/internals/builtins/array/array_key_last.md +++ b/docs/internals/builtins/array/array_key_last.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_last.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_last.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1160](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1160) (`lower_array_key_last`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1331](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1331) (`lower_array_key_last`) - **Function symbol**: `lower_array_key_last()` diff --git a/docs/internals/builtins/array/array_map.md b/docs/internals/builtins/array/array_map.md index 3c8c821bbc..6068f47926 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_map.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:312](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L312) (`lower_array_map`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:318](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L318) (`lower_array_map`) - **Function symbol**: `lower_array_map()` diff --git a/docs/internals/builtins/array/array_merge.md b/docs/internals/builtins/array/array_merge.md index 9212bc0e03..0f1821952c 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:850](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L850) (`lower_array_merge`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:956](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L956) (`lower_array_merge`) - **Function symbol**: `lower_array_merge()` @@ -21,8 +21,7 @@ sidebar: ## Runtime helpers The following runtime helpers are referenced: -- `__rt_array_diff` -- `__rt_array_diff_refcounted` +- `__rt_array_merge_str` ## Signature summary diff --git a/docs/internals/builtins/array/array_merge_recursive.md b/docs/internals/builtins/array/array_merge_recursive.md index 971c2e309c..a8fdb31c68 100644 --- a/docs/internals/builtins/array/array_merge_recursive.md +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_merge_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge_recursive.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1366](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1366) (`lower_array_merge_recursive`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1537](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1537) (`lower_array_merge_recursive`) - **Function symbol**: `lower_array_merge_recursive()` diff --git a/docs/internals/builtins/array/array_multisort.md b/docs/internals/builtins/array/array_multisort.md index 835d460126..c363926001 100644 --- a/docs/internals/builtins/array/array_multisort.md +++ b/docs/internals/builtins/array/array_multisort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_multisort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_multisort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1671](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1671) (`lower_array_multisort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1842](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1842) (`lower_array_multisort`) - **Function symbol**: `lower_array_multisort()` diff --git a/docs/internals/builtins/array/array_pad.md b/docs/internals/builtins/array/array_pad.md index 990472c3bf..9885c9c149 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pad.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:99](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L99) (`lower_array_pad`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:105](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L105) (`lower_array_pad`) - **Function symbol**: `lower_array_pad()` diff --git a/docs/internals/builtins/array/array_pop.md b/docs/internals/builtins/array/array_pop.md index dd4fd2fc26..ddb6323125 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pop.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1048](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1048) (`lower_array_pop`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1205](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1205) (`lower_array_pop`) - **Function symbol**: `lower_array_pop()` @@ -20,9 +20,7 @@ sidebar: ## Runtime helpers -The following runtime helpers are referenced: -- `__rt_sort_int` -- `__rt_sort_str` +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ ## Signature summary diff --git a/docs/internals/builtins/array/array_product.md b/docs/internals/builtins/array/array_product.md index d917bbbb8d..4afc395f24 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -22,6 +22,7 @@ sidebar: The following runtime helpers are referenced: - `__rt_array_product` +- `__rt_array_product_float` ## Signature summary diff --git a/docs/internals/builtins/array/array_push.md b/docs/internals/builtins/array/array_push.md index ef9156d4db..e7aa432953 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_push.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:61](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L61) (`lower_array_push`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:67](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L67) (`lower_array_push`) - **Function symbol**: `lower_array_push()` diff --git a/docs/internals/builtins/array/array_rand.md b/docs/internals/builtins/array/array_rand.md index 108c483596..83fc934bad 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_rand.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1007](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1007) (`lower_array_rand`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1164](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1164) (`lower_array_rand`) - **Function symbol**: `lower_array_rand()` diff --git a/docs/internals/builtins/array/array_reduce.md b/docs/internals/builtins/array/array_reduce.md index 0805b18122..57d03d7acb 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reduce.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:701](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L701) (`lower_array_reduce`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:807](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L807) (`lower_array_reduce`) - **Function symbol**: `lower_array_reduce()` diff --git a/docs/internals/builtins/array/array_replace.md b/docs/internals/builtins/array/array_replace.md index 2223e52af7..8b8500c73e 100644 --- a/docs/internals/builtins/array/array_replace.md +++ b/docs/internals/builtins/array/array_replace.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1328](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1328) (`lower_array_replace`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1499](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1499) (`lower_array_replace`) - **Function symbol**: `lower_array_replace()` diff --git a/docs/internals/builtins/array/array_replace_recursive.md b/docs/internals/builtins/array/array_replace_recursive.md index 443db12e0d..f0b65dd2cb 100644 --- a/docs/internals/builtins/array/array_replace_recursive.md +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_replace_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace_recursive.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1333](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1333) (`lower_array_replace_recursive`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1504](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1504) (`lower_array_replace_recursive`) - **Function symbol**: `lower_array_replace_recursive()` diff --git a/docs/internals/builtins/array/array_reverse.md b/docs/internals/builtins/array/array_reverse.md index d9dd143083..5a63f970e6 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reverse.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:185](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L185) (`lower_array_reverse`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:191](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L191) (`lower_array_reverse`) - **Function symbol**: `lower_array_reverse()` diff --git a/docs/internals/builtins/array/array_search.md b/docs/internals/builtins/array/array_search.md index 957e75529a..651e47243e 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_search.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1710](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1710) (`lower_array_search`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1881](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1881) (`lower_array_search`) - **Function symbol**: `lower_array_search()` diff --git a/docs/internals/builtins/array/array_slice.md b/docs/internals/builtins/array/array_slice.md index d67fd84a15..89410e8f7c 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_slice.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:906](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L906) (`lower_array_slice`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1028](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1028) (`lower_array_slice`) - **Function symbol**: `lower_array_slice()` @@ -20,17 +20,18 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_array_slice_preserve` ## Signature summary ```php -function array_slice(array $array, int $offset, int $length = null): array +function array_slice(array $array, int $offset, int $length = null, bool $preserve_keys = false): array ``` ## What the type checker enforces -- **Arity**: takes 2–3 arguments (1 optional). +- **Arity**: takes 2–4 arguments (2 optional). ## Cross-references diff --git a/docs/internals/builtins/array/array_splice.md b/docs/internals/builtins/array/array_splice.md index 32fc7cf5a3..f5e1cbef19 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_splice.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:949](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L949) (`lower_array_splice`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1106](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1106) (`lower_array_splice`) - **Function symbol**: `lower_array_splice()` diff --git a/docs/internals/builtins/array/array_sum.md b/docs/internals/builtins/array/array_sum.md index 272372ff51..eff6dd28cc 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -22,7 +22,9 @@ sidebar: The following runtime helpers are referenced: - `__rt_array_product` +- `__rt_array_product_float` - `__rt_array_sum` +- `__rt_array_sum_float` ## Signature summary diff --git a/docs/internals/builtins/array/array_udiff.md b/docs/internals/builtins/array/array_udiff.md index a02a45b6a1..5f9ce798d6 100644 --- a/docs/internals/builtins/array/array_udiff.md +++ b/docs/internals/builtins/array/array_udiff.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_udiff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_udiff.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1658](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1658) (`lower_array_udiff`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1829](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1829) (`lower_array_udiff`) - **Function symbol**: `lower_array_udiff()` diff --git a/docs/internals/builtins/array/array_uintersect.md b/docs/internals/builtins/array/array_uintersect.md index a0dc972c89..b09cb78173 100644 --- a/docs/internals/builtins/array/array_uintersect.md +++ b/docs/internals/builtins/array/array_uintersect.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_uintersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_uintersect.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1663](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1663) (`lower_array_uintersect`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1834](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1834) (`lower_array_uintersect`) - **Function symbol**: `lower_array_uintersect()` diff --git a/docs/internals/builtins/array/array_unique.md b/docs/internals/builtins/array/array_unique.md index 6842d2d772..2767572b06 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unique.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:198](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L198) (`lower_array_unique`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L204) (`lower_array_unique`) - **Function symbol**: `lower_array_unique()` diff --git a/docs/internals/builtins/array/array_walk.md b/docs/internals/builtins/array/array_walk.md index fcc57e061f..9b43508ea8 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:783](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L783) (`lower_array_walk`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:889](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L889) (`lower_array_walk`) - **Function symbol**: `lower_array_walk()` diff --git a/docs/internals/builtins/array/array_walk_recursive.md b/docs/internals/builtins/array/array_walk_recursive.md index 8da096fb36..06f000b862 100644 --- a/docs/internals/builtins/array/array_walk_recursive.md +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_walk_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk_recursive.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1542](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1542) (`lower_array_walk_recursive`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1713](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1713) (`lower_array_walk_recursive`) - **Function symbol**: `lower_array_walk_recursive()` diff --git a/docs/internals/builtins/array/arsort.md b/docs/internals/builtins/array/arsort.md index 78baa67b53..25c02003b3 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/arsort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1091](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1091) (`lower_arsort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1262](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1262) (`lower_arsort`) - **Function symbol**: `lower_arsort()` diff --git a/docs/internals/builtins/array/asort.md b/docs/internals/builtins/array/asort.md index 4e29039f27..6600330f07 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/asort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1086](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1086) (`lower_asort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1257](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1257) (`lower_asort`) - **Function symbol**: `lower_asort()` diff --git a/docs/internals/builtins/array/call_user_func.md b/docs/internals/builtins/array/call_user_func.md index bcf9504898..eae594329d 100644 --- a/docs/internals/builtins/array/call_user_func.md +++ b/docs/internals/builtins/array/call_user_func.md @@ -22,7 +22,9 @@ sidebar: The following runtime helpers are referenced: - `__rt_array_product` +- `__rt_array_product_float` - `__rt_array_sum` +- `__rt_array_sum_float` ## Signature summary diff --git a/docs/internals/builtins/array/call_user_func_array.md b/docs/internals/builtins/array/call_user_func_array.md index 325864db80..6ca0ef8528 100644 --- a/docs/internals/builtins/array/call_user_func_array.md +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -22,7 +22,9 @@ sidebar: The following runtime helpers are referenced: - `__rt_array_product` +- `__rt_array_product_float` - `__rt_array_sum` +- `__rt_array_sum_float` ## Signature summary diff --git a/docs/internals/builtins/array/in_array.md b/docs/internals/builtins/array/in_array.md index 39eda2e411..41aa0f419c 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/in_array.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1729](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1729) (`lower_in_array`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1902](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1902) (`lower_in_array`) - **Function symbol**: `lower_in_array()` diff --git a/docs/internals/builtins/array/krsort.md b/docs/internals/builtins/array/krsort.md index 76024a8418..00e9bbf02e 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/krsort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1101](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1101) (`lower_krsort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1272](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1272) (`lower_krsort`) - **Function symbol**: `lower_krsort()` diff --git a/docs/internals/builtins/array/ksort.md b/docs/internals/builtins/array/ksort.md index 07772337ab..9cd9147a37 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/ksort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1096](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1096) (`lower_ksort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1267](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1267) (`lower_ksort`) - **Function symbol**: `lower_ksort()` diff --git a/docs/internals/builtins/array/natcasesort.md b/docs/internals/builtins/array/natcasesort.md index 882acdfa46..7a1d9fcbdc 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natcasesort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1111](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1111) (`lower_natcasesort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1282](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1282) (`lower_natcasesort`) - **Function symbol**: `lower_natcasesort()` diff --git a/docs/internals/builtins/array/natsort.md b/docs/internals/builtins/array/natsort.md index 271ed4fc8b..50d9935823 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natsort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1106](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1106) (`lower_natsort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1277](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1277) (`lower_natsort`) - **Function symbol**: `lower_natsort()` diff --git a/docs/internals/builtins/array/range.md b/docs/internals/builtins/array/range.md index c99ae386b3..b4b1c4d640 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/range.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1020](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1020) (`lower_range`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1177](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1177) (`lower_range`) - **Function symbol**: `lower_range()` diff --git a/docs/internals/builtins/array/rsort.md b/docs/internals/builtins/array/rsort.md index f1b01e1e70..7b675fa3e2 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/rsort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1081](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1081) (`lower_rsort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1245](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1245) (`lower_rsort`) - **Function symbol**: `lower_rsort()` @@ -25,7 +25,7 @@ The following runtime helpers are referenced: - `__rt_asort` - `__rt_krsort` - `__rt_ksort` -- `__rt_natsort` +- `__rt_rsort_float` - `__rt_rsort_int` - `__rt_rsort_str` diff --git a/docs/internals/builtins/array/shuffle.md b/docs/internals/builtins/array/shuffle.md index e2b170a489..ab5b6e4d80 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/shuffle.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1116](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1116) (`lower_shuffle`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1287](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1287) (`lower_shuffle`) - **Function symbol**: `lower_shuffle()` diff --git a/docs/internals/builtins/array/sort.md b/docs/internals/builtins/array/sort.md index 5e8cc189b3..ee40b752dc 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/sort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1076](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1076) (`lower_sort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1233](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1233) (`lower_sort`) - **Function symbol**: `lower_sort()` @@ -23,10 +23,10 @@ sidebar: The following runtime helpers are referenced: - `__rt_arsort` - `__rt_asort` -- `__rt_krsort` -- `__rt_ksort` +- `__rt_rsort_float` - `__rt_rsort_int` - `__rt_rsort_str` +- `__rt_sort_float` - `__rt_sort_int` - `__rt_sort_str` diff --git a/docs/internals/builtins/array/uasort.md b/docs/internals/builtins/array/uasort.md index 5e8fffe9b1..202dc1ec74 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uasort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1131](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1131) (`lower_uasort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1302](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1302) (`lower_uasort`) - **Function symbol**: `lower_uasort()` diff --git a/docs/internals/builtins/array/uksort.md b/docs/internals/builtins/array/uksort.md index 23a5018555..79adcd7440 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uksort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1126](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1126) (`lower_uksort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1297](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1297) (`lower_uksort`) - **Function symbol**: `lower_uksort()` diff --git a/docs/internals/builtins/array/usort.md b/docs/internals/builtins/array/usort.md index 9ef5531dda..e47eed3099 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/usort.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1121](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1121) (`lower_usort`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1292](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1292) (`lower_usort`) - **Function symbol**: `lower_usort()` diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index ff8eadd710..4492d230c1 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_classes.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:484](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L484) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index 4f58c119bb..0f776569b9 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_interfaces.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:484](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L484) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index 9ea12d0868..0ba66a350f 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_traits.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:484](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L484) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/is_a.md b/docs/internals/builtins/class/is_a.md index b7471d9035..d5c79f6c12 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -16,7 +16,7 @@ sidebar: ### Lowering notes -- Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets. +- Lowers `is_a()` and `is_subclass_of()` for object, boxed-Mixed, and string-class-name operands. ## Runtime helpers diff --git a/docs/internals/builtins/class/is_subclass_of.md b/docs/internals/builtins/class/is_subclass_of.md index c1ac2885a5..3e0111d157 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -16,7 +16,7 @@ sidebar: ### Lowering notes -- Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets. +- Lowers `is_a()` and `is_subclass_of()` for object, boxed-Mixed, and string-class-name operands. ## Runtime helpers diff --git a/docs/internals/builtins/math/max.md b/docs/internals/builtins/math/max.md index d27f5aae58..d1484be894 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/max.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L204) (`lower_min_max`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:228](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L228) (`lower_min_max`) - **Function symbol**: `lower_min_max()` @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_mixed_cast_float` ## Signature summary diff --git a/docs/internals/builtins/math/min.md b/docs/internals/builtins/math/min.md index 4125f3b182..3903a93afc 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/min.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L204) (`lower_min_max`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:228](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L228) (`lower_min_max`) - **Function symbol**: `lower_min_max()` @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_mixed_cast_float` ## Signature summary diff --git a/docs/internals/builtins/math/pi.md b/docs/internals/builtins/math/pi.md index b7396b3ebe..a7f5d607a5 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pi.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:240](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L240) (`lower_pi`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:267](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L267) (`lower_pi`) - **Function symbol**: `lower_pi()` diff --git a/docs/internals/builtins/math/round.md b/docs/internals/builtins/math/round.md index a39f1d9a34..3b192c05ef 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -25,12 +25,12 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function round(float $num, int $precision = 0): float +function round(float $num, int $precision = 0, int $mode = 1): float ``` ## What the type checker enforces -- **Arity**: takes 1–2 arguments (1 optional). +- **Arity**: takes 1–3 arguments (2 optional). ## Cross-references diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index b9702e7dbe..e2b36d10b6 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chr.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:858](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L858) (`lower_chr`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:857](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L857) (`lower_chr`) - **Function symbol**: `lower_chr()` diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index 9507e07c8f..8c0ad0476e 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/number_format.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:875](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L875) (`lower_number_format`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:874](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L874) (`lower_number_format`) - **Function symbol**: `lower_number_format()` diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index d37d48838d..117f3d4607 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ord.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:834](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L834) (`lower_ord`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:833](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L833) (`lower_ord`) - **Function symbol**: `lower_ord()` diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index f70a7efe9e..a18b62adb7 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ireplace.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L780) (`lower_string_replace`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:779](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L779) (`lower_string_replace`) - **Function symbol**: `lower_string_replace()` diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index 095b3407ff..b616c33a5c 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_pad.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:818](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L818) (`lower_str_pad`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:817](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L817) (`lower_str_pad`) - **Function symbol**: `lower_str_pad()` diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index a40756e500..e360bdf096 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_repeat.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:746](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L746) (`lower_str_repeat`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:745](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L745) (`lower_str_repeat`) - **Function symbol**: `lower_str_repeat()` diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 6e901922ce..d7c17401ef 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_replace.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L780) (`lower_string_replace`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:779](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L779) (`lower_string_replace`) - **Function symbol**: `lower_string_replace()` diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index c2b4545e61..106023d265 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strstr.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:762](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L762) (`lower_strstr`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:761](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L761) (`lower_strstr`) - **Function symbol**: `lower_strstr()` diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index f2e4d9f7e3..59d845cf6d 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_replace.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:730](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L730) (`lower_substr_replace`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:729](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L729) (`lower_substr_replace`) - **Function symbol**: `lower_substr_replace()` diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 88345fccd8..f31d3e5eab 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/wordwrap.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:802](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L802) (`lower_wordwrap`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:801](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L801) (`lower_wordwrap`) - **Function symbol**: `lower_wordwrap()` diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index a942471603..c9e1f8c694 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_id.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:424](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L424) (`lower_get_resource_id`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:520](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L520) (`lower_get_resource_id`) - **Function symbol**: `lower_get_resource_id()` diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index fcda18f145..c1ee859cd0 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_type.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:412](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L412) (`lower_get_resource_type`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:508](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L508) (`lower_get_resource_type`) - **Function symbol**: `lower_get_resource_type()` diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 70e5bf456e..1328e03b06 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_resource.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:400](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L400) (`lower_is_resource`) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:496](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L496) (`lower_is_resource`) - **Function symbol**: `lower_is_resource()` diff --git a/docs/php/arrays.md b/docs/php/arrays.md index 36e02128bd..1cdd7a6ecc 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -202,7 +202,7 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `array_slice()` | `array_slice($arr, $offset [, $length]): array` | Extract a slice | | `array_splice()` | `array_splice($arr, $offset [, $length]): array` | Remove a slice in place and return the removed elements | | `array_chunk()` | `array_chunk($arr, $size): array` | Split into chunks | -| `array_merge()` | `array_merge($arr1, $arr2): array` | Merge two arrays | +| `array_merge()` | `array_merge($arr1, $arr2, ...): array` | Merge two or more arrays (concatenated left to right) | | `array_merge_recursive()` | `array_merge_recursive($arr1, $arr2): array` | Recursively merge two arrays: integer keys append (renumbered), string keys that collide recurse when both values are arrays and otherwise combine into a list. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool); nested indexed-array values are treated as opaque. | | `array_replace()` | `array_replace($arr, $replacements): array` | Overwrite matching keys in `$arr` (in place, keeping position) and append new keys from `$replacements`; later values win. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool). | | `array_replace_recursive()` | `array_replace_recursive($arr, $replacements): array` | Like `array_replace()`, but when both values at a key are associative arrays they are merged recursively instead of overwritten. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool); nested indexed arrays are overwritten, not merged. | @@ -211,8 +211,8 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `array_fill_keys()` | `array_fill_keys($keys, $value): array` | Fill with values using keys | | `array_pad()` | `array_pad($arr, $size, $value): array` | Pad to length | | `range()` | `range($start, $end): array` | Sequential integers | -| `array_diff()` | `array_diff($arr1, $arr2): array` | Values in $arr1 not in $arr2 | -| `array_intersect()` | `array_intersect($arr1, $arr2): array` | Values in both | +| `array_diff()` | `array_diff($arr1, $arr2, ...): array` | Values in $arr1 not present in any of the other arrays | +| `array_intersect()` | `array_intersect($arr1, $arr2, ...): array` | Values present in every array | | `array_diff_key()` | `array_diff_key($arr1, $arr2): array` | Keys in $arr1 not in $arr2 | | `array_intersect_key()` | `array_intersect_key($arr1, $arr2): array` | Keys in both | | `array_diff_assoc()` | `array_diff_assoc($arr1, $arr2): array` | Entries of $arr1 whose `(key, value)` pair is absent from $arr2 (values compared as `(string)$a === (string)$b`). Accepts associative arrays or **indexed arrays of scalars** (int/float/bool). | diff --git a/docs/php/builtins.md b/docs/php/builtins.md index 94d0c453ec..e930ad4742 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -14,7 +14,7 @@ sidebar: | [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | | [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | | [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | +| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | | [`array_fill()`](./builtins/array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | @@ -22,7 +22,7 @@ sidebar: | [`array_filter()`](./builtins/array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | | [`array_find()`](./builtins/array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | | [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | +| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_intersect_assoc()`](./builtins/array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_intersect_key()`](./builtins/array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | | [`array_is_list()`](./builtins/array/array_is_list.md) | `(mixed $array): bool` | `bool` | @@ -45,7 +45,7 @@ sidebar: | [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | | [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | | [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | +| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null, bool $preserve_keys = false): array` | `array` | | [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | | [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | | [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | @@ -273,7 +273,7 @@ sidebar: | [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | | [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | | [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | +| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0, int $mode = 1): float` | `float` | | [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | | [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | | [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index 683b2026b7..7472e2f5fd 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -14,7 +14,7 @@ sidebar: | [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | | [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | | [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | +| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | | [`array_fill()`](./array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | @@ -22,7 +22,7 @@ sidebar: | [`array_filter()`](./array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | | [`array_find()`](./array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | | [`array_flip()`](./array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | +| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_intersect_assoc()`](./array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | | [`array_intersect_key()`](./array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | | [`array_is_list()`](./array/array_is_list.md) | `(mixed $array): bool` | `bool` | @@ -45,7 +45,7 @@ sidebar: | [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | | [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | | [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | +| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null, bool $preserve_keys = false): array` | `array` | | [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | | [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | | [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | diff --git a/docs/php/builtins/array/array_diff.md b/docs/php/builtins/array/array_diff.md index ea514aa384..6e018d32ee 100644 --- a/docs/php/builtins/array/array_diff.md +++ b/docs/php/builtins/array/array_diff.md @@ -8,7 +8,7 @@ sidebar: ## array_diff() ```php -function array_diff(array $array, ...$arrays): array +function array_diff(array $array, ...$arrays): mixed ``` Computes the difference of arrays. @@ -17,7 +17,7 @@ Computes the difference of arrays. - `$array` (`array`) - `...$arrays` — variadic: collects excess arguments into `$arrays`. -**Returns**: `array` +**Returns**: `mixed` _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect.md b/docs/php/builtins/array/array_intersect.md index 55c5ce3ab5..7a2546b0cf 100644 --- a/docs/php/builtins/array/array_intersect.md +++ b/docs/php/builtins/array/array_intersect.md @@ -8,7 +8,7 @@ sidebar: ## array_intersect() ```php -function array_intersect(array $array, ...$arrays): array +function array_intersect(array $array, ...$arrays): mixed ``` Computes the intersection of arrays. @@ -17,7 +17,7 @@ Computes the intersection of arrays. - `$array` (`array`) - `...$arrays` — variadic: collects excess arguments into `$arrays`. -**Returns**: `array` +**Returns**: `mixed` _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_slice.md b/docs/php/builtins/array/array_slice.md index 8abb0326ca..d804eb2645 100644 --- a/docs/php/builtins/array/array_slice.md +++ b/docs/php/builtins/array/array_slice.md @@ -8,7 +8,7 @@ sidebar: ## array_slice() ```php -function array_slice(array $array, int $offset, int $length = null): array +function array_slice(array $array, int $offset, int $length = null, bool $preserve_keys = false): array ``` Extracts a slice of an array. @@ -17,6 +17,7 @@ Extracts a slice of an array. - `$array` (`array`) - `$offset` (`int`) - `$length` (`int`), default `null`, optional +- `$preserve_keys` (`bool`), default `false`, optional **Returns**: `array` diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index c4b4f8457b..4c1a8ea0be 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -39,7 +39,7 @@ sidebar: | [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | | [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | | [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | +| [`round()`](./math/round.md) | `(float $num, int $precision = 0, int $mode = 1): float` | `float` | | [`sin()`](./math/sin.md) | `(float $num): float` | `float` | | [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | | [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index 3364d0c5fc..d595218f46 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -8,7 +8,7 @@ sidebar: ## round() ```php -function round(float $num, int $precision = 0): float +function round(float $num, int $precision = 0, int $mode = 1): float ``` Rounds a float. @@ -16,6 +16,7 @@ Rounds a float. **Parameters**: - `$num` (`float`) - `$precision` (`int`), default `0`, optional +- `$mode` (`int`), default `1`, optional **Returns**: `float` diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 5f3d2b5c88..bbe253bcb6 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -969,7 +969,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_all", - "codegen_line": 1536, + "codegen_line": 1707, "notes": [ "Lowers `array_all()`: returns true when every element satisfies the predicate." ], @@ -1016,7 +1016,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_any", - "codegen_line": 1531, + "codegen_line": 1702, "notes": [ "Lowers `array_any()`: returns true when some element satisfies the predicate." ], @@ -1062,7 +1062,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_chunk", - "codegen_line": 81, + "codegen_line": 87, "notes": [ "Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays." ], @@ -1150,7 +1150,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_combine", - "codegen_line": 152, + "codegen_line": 158, "notes": [ "Lowers `array_combine()` through the legacy hash-building runtime helpers." ], @@ -1194,7 +1194,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_diff", - "codegen_line": 874, + "codegen_line": 996, "notes": [ "Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots." ], @@ -1221,7 +1221,7 @@ "type": "array" } ], - "return_type": "array", + "return_type": "mixed", "variadic": "arrays" }, "slug": "array_diff", @@ -1238,7 +1238,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_diff_assoc", - "codegen_line": 1347, + "codegen_line": 1518, "notes": [ "Lowers `array_diff_assoc()` via the shared associative diff/intersect helper (mode 0 = diff)." ], @@ -1278,7 +1278,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_diff_key", - "codegen_line": 896, + "codegen_line": 1018, "notes": [ "Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys." ], @@ -1318,7 +1318,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_fill", - "codegen_line": 115, + "codegen_line": 121, "notes": [ "Lowers `array_fill()` for pointer-sized scalar and refcounted payloads." ], @@ -1369,7 +1369,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_fill_keys", - "codegen_line": 138, + "codegen_line": 144, "notes": [ "Lowers `array_fill_keys()` through the legacy hash-building runtime helpers." ], @@ -1413,7 +1413,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_filter", - "codegen_line": 211, + "codegen_line": 217, "notes": [ "Lowers `array_filter()` for static and first-class callbacks through the runtime helper." ], @@ -1467,7 +1467,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_find", - "codegen_line": 1526, + "codegen_line": 1697, "notes": [ "Lowers `array_find()`: returns the first element satisfying the predicate, boxed as Mixed (or null)." ], @@ -1513,7 +1513,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_flip", - "codegen_line": 171, + "codegen_line": 177, "notes": [ "Lowers `array_flip()` through the legacy hash-building runtime helpers." ], @@ -1550,7 +1550,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_intersect", - "codegen_line": 885, + "codegen_line": 1007, "notes": [ "Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots." ], @@ -1575,7 +1575,7 @@ "type": "array" } ], - "return_type": "array", + "return_type": "mixed", "variadic": "arrays" }, "slug": "array_intersect", @@ -1592,7 +1592,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_intersect_assoc", - "codegen_line": 1352, + "codegen_line": 1523, "notes": [ "Lowers `array_intersect_assoc()` via the shared associative diff/intersect helper (mode 1 = intersect)." ], @@ -1634,7 +1634,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_intersect_key", - "codegen_line": 901, + "codegen_line": 1023, "notes": [ "Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys." ], @@ -1673,7 +1673,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_is_list", - "codegen_line": 1144, + "codegen_line": 1315, "notes": [ "Lowers `array_is_list()` to the `__rt_array_is_list` runtime predicate, returning a bool.", "The runtime helper accepts any array kind (indexed, associative hash, or boxed mixed cell) and", @@ -1760,7 +1760,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_key_first", - "codegen_line": 1155, + "codegen_line": 1326, "notes": [ "Lowers `array_key_first()` through the shared edge-key helper with selector `0`." ], @@ -1800,7 +1800,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_key_last", - "codegen_line": 1160, + "codegen_line": 1331, "notes": [ "Lowers `array_key_last()` through the shared edge-key helper with selector `1`." ], @@ -1877,7 +1877,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_map", - "codegen_line": 312, + "codegen_line": 318, "notes": [ "Lowers `array_map()` through the callback runtime helper matching the callback result type." ], @@ -1921,13 +1921,12 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_merge", - "codegen_line": 850, + "codegen_line": 956, "notes": [ "Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots." ], "runtime_helpers": [ - "__rt_array_diff", - "__rt_array_diff_refcounted" + "__rt_array_merge_str" ], "sig_arm": null, "sig_file": "src/builtins/array/array_merge.rs", @@ -1953,7 +1952,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_merge_recursive", - "codegen_line": 1366, + "codegen_line": 1537, "notes": [ "Lowers `array_merge_recursive()` (recursive merge with scalar collisions combined into lists)." ], @@ -1986,7 +1985,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_multisort", - "codegen_line": 1671, + "codegen_line": 1842, "notes": [ "Lowers `array_multisort()`: stable-sorts the first indexed array ascending and reorders the second", "in tandem, both in place. Both arguments are by-reference, so each is copy-on-write split with", @@ -2035,7 +2034,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_pad", - "codegen_line": 99, + "codegen_line": 105, "notes": [ "Lowers `array_pad()` by copying an indexed array and filling missing slots." ], @@ -2086,14 +2085,11 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_pop", - "codegen_line": 1048, + "codegen_line": 1205, "notes": [ "Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed." ], - "runtime_helpers": [ - "__rt_sort_int", - "__rt_sort_str" - ], + "runtime_helpers": [], "sig_arm": null, "sig_file": "src/builtins/array/array_pop.rs", "sig_line": null @@ -2131,7 +2127,8 @@ "Lowers `array_product()` over supported indexed-array payloads." ], "runtime_helpers": [ - "__rt_array_product" + "__rt_array_product", + "__rt_array_product_float" ], "sig_arm": null, "sig_file": "src/builtins/array/array_product.rs", @@ -2165,7 +2162,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_push", - "codegen_line": 61, + "codegen_line": 67, "notes": [ "Lowers `array_push()` by appending one value and publishing the mutated array." ], @@ -2202,7 +2199,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_rand", - "codegen_line": 1007, + "codegen_line": 1164, "notes": [ "Lowers `array_rand()` for indexed arrays." ], @@ -2242,7 +2239,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_reduce", - "codegen_line": 701, + "codegen_line": 807, "notes": [ "Lowers `array_reduce()` through the callback-driven runtime helper." ], @@ -2295,7 +2292,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_replace", - "codegen_line": 1328, + "codegen_line": 1499, "notes": [ "Lowers `array_replace()` (right-wins hash merge of two hashes)." ], @@ -2343,7 +2340,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_replace_recursive", - "codegen_line": 1333, + "codegen_line": 1504, "notes": [ "Lowers `array_replace_recursive()` (recursive right-wins hash merge)." ], @@ -2390,7 +2387,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_reverse", - "codegen_line": 185, + "codegen_line": 191, "notes": [ "Lowers `array_reverse()` for indexed arrays with 8-byte payload slots." ], @@ -2427,7 +2424,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_search", - "codegen_line": 1710, + "codegen_line": 1881, "notes": [ "Lowers `array_search()` for indexed arrays with integer-like payloads." ], @@ -2515,11 +2512,13 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_slice", - "codegen_line": 906, + "codegen_line": 1028, "notes": [ "Lowers `array_slice()` for indexed arrays with pointer-sized payload slots." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_array_slice_preserve" + ], "sig_arm": null, "sig_file": "src/builtins/array/array_slice.rs", "sig_line": null @@ -2547,6 +2546,13 @@ "name": "length", "optional": true, "type": "int" + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true, + "type": "bool" } ], "return_type": "array", @@ -2566,7 +2572,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_splice", - "codegen_line": 949, + "codegen_line": 1106, "notes": [ "Lowers `array_splice()` by mutating an indexed source array and returning removed elements." ], @@ -2623,7 +2629,9 @@ ], "runtime_helpers": [ "__rt_array_product", - "__rt_array_sum" + "__rt_array_product_float", + "__rt_array_sum", + "__rt_array_sum_float" ], "sig_arm": null, "sig_file": "src/builtins/array/array_sum.rs", @@ -2657,7 +2665,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_udiff", - "codegen_line": 1658, + "codegen_line": 1829, "notes": [ "Lowers `array_udiff()`: keeps first-array elements not equal (per comparator) to any second-array element." ], @@ -2708,7 +2716,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_uintersect", - "codegen_line": 1663, + "codegen_line": 1834, "notes": [ "Lowers `array_uintersect()`: keeps first-array elements equal (per comparator) to some second-array element." ], @@ -2759,7 +2767,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_unique", - "codegen_line": 198, + "codegen_line": 204, "notes": [ "Lowers `array_unique()` for indexed arrays with 8-byte payload slots." ], @@ -2873,7 +2881,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_walk", - "codegen_line": 783, + "codegen_line": 889, "notes": [ "Lowers `array_walk()` through the callback-driven runtime helper." ], @@ -2919,7 +2927,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_walk_recursive", - "codegen_line": 1542, + "codegen_line": 1713, "notes": [ "Lowers `array_walk_recursive()`: invokes the callback on each scalar leaf of a (possibly nested)", "array, descending into array-valued elements. Returns void; leaves are passed as 8-byte scalars." @@ -2967,7 +2975,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_arsort", - "codegen_line": 1091, + "codegen_line": 1262, "notes": [ "Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper." ], @@ -3047,7 +3055,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_asort", - "codegen_line": 1086, + "codegen_line": 1257, "notes": [ "Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper." ], @@ -3488,7 +3496,9 @@ ], "runtime_helpers": [ "__rt_array_product", - "__rt_array_sum" + "__rt_array_product_float", + "__rt_array_sum", + "__rt_array_sum_float" ], "sig_arm": null, "sig_file": "src/builtins/callables/call_user_func.rs", @@ -3528,7 +3538,9 @@ ], "runtime_helpers": [ "__rt_array_product", - "__rt_array_sum" + "__rt_array_product_float", + "__rt_array_sum", + "__rt_array_sum_float" ], "sig_arm": null, "sig_file": "src/builtins/callables/call_user_func_array.rs", @@ -3886,7 +3898,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_chr", - "codegen_line": 858, + "codegen_line": 857, "notes": [ "Lowers `chr()` by converting an integer code point into a one-byte string." ], @@ -7223,7 +7235,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 484, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7252,7 +7264,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 484, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7281,7 +7293,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 484, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7347,7 +7359,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_id", - "codegen_line": 424, + "codegen_line": 520, "notes": [ "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." ], @@ -7384,7 +7396,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_type", - "codegen_line": 412, + "codegen_line": 508, "notes": [ "Lowers `get_resource_type(resource)` to elephc's current resource type label." ], @@ -9065,7 +9077,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_in_array", - "codegen_line": 1729, + "codegen_line": 1902, "notes": [ "Lowers `in_array()` for indexed arrays with scalar or string payloads." ], @@ -9364,7 +9376,7 @@ "codegen_function": "lower_is_a_relation", "codegen_line": 369, "notes": [ - "Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets." + "Lowers `is_a()` and `is_subclass_of()` for object, boxed-Mixed, and string-class-name operands." ], "runtime_helpers": [], "sig_arm": null, @@ -10071,7 +10083,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", "codegen_function": "lower_is_resource", - "codegen_line": 400, + "codegen_line": 496, "notes": [ "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." ], @@ -10186,7 +10198,7 @@ "codegen_function": "lower_is_a_relation", "codegen_line": 369, "notes": [ - "Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets." + "Lowers `is_a()` and `is_subclass_of()` for object, boxed-Mixed, and string-class-name operands." ], "runtime_helpers": [], "sig_arm": null, @@ -10713,7 +10725,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_krsort", - "codegen_line": 1101, + "codegen_line": 1272, "notes": [ "Lowers `krsort()` through the legacy reverse key-sort helper surface." ], @@ -10754,7 +10766,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_ksort", - "codegen_line": 1096, + "codegen_line": 1267, "notes": [ "Lowers `ksort()` through the legacy key-sort helper surface." ], @@ -11312,11 +11324,13 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", "codegen_function": "lower_min_max", - "codegen_line": 204, + "codegen_line": 228, "notes": [ "Lowers numeric `min()` and `max()` over concrete integer-like or float operands." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_mixed_cast_float" + ], "sig_arm": null, "sig_file": "src/builtins/math/max.rs", "sig_line": null @@ -11445,11 +11459,13 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", "codegen_function": "lower_min_max", - "codegen_line": 204, + "codegen_line": 228, "notes": [ "Lowers numeric `min()` and `max()` over concrete integer-like or float operands." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_mixed_cast_float" + ], "sig_arm": null, "sig_file": "src/builtins/math/min.rs", "sig_line": null @@ -11647,7 +11663,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_natcasesort", - "codegen_line": 1111, + "codegen_line": 1282, "notes": [ "Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper." ], @@ -11687,7 +11703,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_natsort", - "codegen_line": 1106, + "codegen_line": 1277, "notes": [ "Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper." ], @@ -11767,7 +11783,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_number_format", - "codegen_line": 875, + "codegen_line": 874, "notes": [ "Lowers `number_format()` by arranging its runtime helper arguments." ], @@ -11868,7 +11884,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_ord", - "codegen_line": 834, + "codegen_line": 833, "notes": [ "Lowers `ord()` by returning the first byte of a string or zero for empty input." ], @@ -12163,7 +12179,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", "codegen_function": "lower_pi", - "codegen_line": 240, + "codegen_line": 267, "notes": [ "Lowers `pi()` as the same data-section float constant used by the legacy backend." ], @@ -13398,7 +13414,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_range", - "codegen_line": 1020, + "codegen_line": 1177, "notes": [ "Lowers `range()` for integer endpoints through the shared runtime constructor." ], @@ -13975,6 +13991,13 @@ "name": "precision", "optional": true, "type": "int" + }, + { + "by_ref": false, + "default": "1", + "name": "mode", + "optional": true, + "type": "int" } ], "return_type": "float", @@ -13994,7 +14017,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_rsort", - "codegen_line": 1081, + "codegen_line": 1245, "notes": [ "Lowers `rsort()` for indexed integer arrays by mutating the source array in place." ], @@ -14003,7 +14026,7 @@ "__rt_asort", "__rt_krsort", "__rt_ksort", - "__rt_natsort", + "__rt_rsort_float", "__rt_rsort_int", "__rt_rsort_str" ], @@ -14295,7 +14318,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_shuffle", - "codegen_line": 1116, + "codegen_line": 1287, "notes": [ "Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place." ], @@ -14447,17 +14470,17 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_sort", - "codegen_line": 1076, + "codegen_line": 1233, "notes": [ "Lowers `sort()` for indexed integer arrays by mutating the source array in place." ], "runtime_helpers": [ "__rt_arsort", "__rt_asort", - "__rt_krsort", - "__rt_ksort", + "__rt_rsort_float", "__rt_rsort_int", "__rt_rsort_str", + "__rt_sort_float", "__rt_sort_int", "__rt_sort_str" ], @@ -15093,7 +15116,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 780, + "codegen_line": 779, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -15151,7 +15174,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_pad", - "codegen_line": 818, + "codegen_line": 817, "notes": [ "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." ], @@ -15211,7 +15234,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_repeat", - "codegen_line": 746, + "codegen_line": 745, "notes": [ "Lowers `str_repeat(string, times)` through the shared runtime helper." ], @@ -15257,7 +15280,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 780, + "codegen_line": 779, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -17705,7 +17728,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_strstr", - "codegen_line": 762, + "codegen_line": 761, "notes": [ "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." ], @@ -17941,7 +17964,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr_replace", - "codegen_line": 730, + "codegen_line": 729, "notes": [ "Lowers `substr_replace(string, replacement, start, length?)`." ], @@ -18446,7 +18469,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_uasort", - "codegen_line": 1131, + "codegen_line": 1302, "notes": [ "Lowers `uasort()` through the legacy user-sort helper for static comparators." ], @@ -18578,7 +18601,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_uksort", - "codegen_line": 1126, + "codegen_line": 1297, "notes": [ "Lowers `uksort()` through the legacy user-sort helper for static comparators." ], @@ -18911,7 +18934,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", "codegen_function": "lower_usort", - "codegen_line": 1121, + "codegen_line": 1292, "notes": [ "Lowers `usort()` for indexed integer arrays with a static user comparator." ], @@ -19140,7 +19163,7 @@ "checker_line": null, "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", "codegen_function": "lower_wordwrap", - "codegen_line": 802, + "codegen_line": 801, "notes": [ "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper." ], diff --git a/src/builtins/array/array_diff.rs b/src/builtins/array/array_diff.rs index 247d1ac0f9..b85f51826c 100644 --- a/src/builtins/array/array_diff.rs +++ b/src/builtins/array/array_diff.rs @@ -7,12 +7,11 @@ //! //! Key details: //! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` -//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, -//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; -//! `function_sig` and the parity gate keep the variadic shape from the golden. -//! - `check` reproduces the legacy rule: the first argument must be an indexed or -//! associative array, and the result preserves that first-operand type. A check hook -//! is required because the return type depends on the inferred first-argument type. +//! param plus a variadic `arrays`). `min_args: 2` enforces at least two arrays; the variadic +//! shape (unbounded max) accepts three or more (`a \ b \ c`), matching PHP. +//! - `check` requires every argument to be an indexed or associative array; the result preserves +//! the first-operand type (`array_diff(a, b, c)` keeps `a`'s shape). A three-plus-array call is +//! lowered by rewriting it into left-nested two-array calls (see `src/ir_lower/expr/mod.rs`). //! - `lower` is a thin wrapper over the shared `arrays::lower_array_diff` emitter. use crate::builtins::spec::BuiltinCheckCtx; @@ -28,7 +27,6 @@ builtin! { params: [array: Mixed], variadic: "arrays", min_args: 2, - max_args: 2, returns: Mixed, check: check, lower: lower, @@ -36,20 +34,27 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.array-diff.php", } -/// Validates the first argument is an array and returns its (preserved) type. +/// Validates every argument is an array and returns the first operand's (preserved) type. /// -/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is -/// re-inferred here to drive the return type; the registry already inferred every -/// argument once for side effects. The result preserves the first-operand array shape. +/// Arity (>= 2 args) is pre-validated by `check_arity`. Every argument must be an array +/// (`array_diff(a, b, c)` compares `a` against both `b` and `c`); the result preserves the +/// first-operand array shape. Types are re-inferred here to validate and drive the return type; +/// the registry already inferred every argument once for side effects. fn check(cx: &mut BuiltinCheckCtx) -> Result { - let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; - if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { - return Err(CompileError::new( - cx.span, - &format!("{}() first argument must be array", cx.name), - )); + let mut first = None; + for arg in cx.args { + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() arguments must be arrays", cx.name), + )); + } + if first.is_none() { + first = Some(ty); + } } - Ok(ty1) + Ok(first.expect("array_diff requires at least two array arguments")) } /// Lowers an `array_diff` call by dispatching to the shared array emitter. diff --git a/src/builtins/array/array_intersect.rs b/src/builtins/array/array_intersect.rs index d1348c281c..266adceb48 100644 --- a/src/builtins/array/array_intersect.rs +++ b/src/builtins/array/array_intersect.rs @@ -7,12 +7,11 @@ //! //! Key details: //! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` -//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, -//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; -//! `function_sig` and the parity gate keep the variadic shape from the golden. -//! - `check` reproduces the legacy rule: the first argument must be an indexed or -//! associative array, and the result preserves that first-operand type. A check hook -//! is required because the return type depends on the inferred first-argument type. +//! param plus a variadic `arrays`). `min_args: 2` enforces at least two arrays; the variadic +//! shape (unbounded max) accepts three or more (`a ∩ b ∩ c`), matching PHP. +//! - `check` requires every argument to be an indexed or associative array; the result preserves +//! the first-operand type. A three-plus-array call is lowered by rewriting it into left-nested +//! two-array calls (see `src/ir_lower/expr/mod.rs`). //! - `lower` is a thin wrapper over the shared `arrays::lower_array_intersect` emitter. use crate::builtins::spec::BuiltinCheckCtx; @@ -28,7 +27,6 @@ builtin! { params: [array: Mixed], variadic: "arrays", min_args: 2, - max_args: 2, returns: Mixed, check: check, lower: lower, @@ -36,20 +34,27 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.array-intersect.php", } -/// Validates the first argument is an array and returns its (preserved) type. +/// Validates every argument is an array and returns the first operand's (preserved) type. /// -/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is -/// re-inferred here to drive the return type; the registry already inferred every -/// argument once for side effects. The result preserves the first-operand array shape. +/// Arity (>= 2 args) is pre-validated by `check_arity`. Every argument must be an array +/// (`array_intersect(a, b, c)` intersects `a` with both `b` and `c`); the result preserves the +/// first-operand array shape. Types are re-inferred here to validate and drive the return type; +/// the registry already inferred every argument once for side effects. fn check(cx: &mut BuiltinCheckCtx) -> Result { - let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; - if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { - return Err(CompileError::new( - cx.span, - &format!("{}() first argument must be array", cx.name), - )); + let mut first = None; + for arg in cx.args { + let ty = cx.checker.infer_type(arg, cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() arguments must be arrays", cx.name), + )); + } + if first.is_none() { + first = Some(ty); + } } - Ok(ty1) + Ok(first.expect("array_intersect requires at least two array arguments")) } /// Lowers an `array_intersect` call by dispatching to the shared array emitter. diff --git a/src/builtins/array/array_map.rs b/src/builtins/array/array_map.rs index 6c6e6ff8b4..d76bc8fa09 100644 --- a/src/builtins/array/array_map.rs +++ b/src/builtins/array/array_map.rs @@ -7,12 +7,13 @@ //! //! Key details: //! - The PHP golden signature is `variadic(&["callback","array"], "arrays")` (two -//! required params plus a variadic `arrays`). The legacy CHECK arm required exactly -//! 2 arguments, so `min_args: 2, max_args: 2` reproduce that enforcement in -//! `check_arity` only; `function_sig` and the parity gate keep the variadic shape. +//! required params plus a variadic `arrays`). `min_args: 2, max_args: 3` bound +//! `check_arity` to the single-array form plus the supported two-input-array form +//! (`array_map($cb, $a, $b)`); `function_sig` and the parity gate keep the variadic shape. //! - `check` validates that the second argument is an indexed array and infers the //! callback return element type; the result preserves the input array element type -//! unless the callback returns Mixed. +//! unless the callback returns Mixed. The two-input-array form defers to +//! `check_array_map_multi`, which enforces the bounded (int/str, shared-type) subset. //! - `lower` is a thin wrapper over the shared `arrays::lower_array_map` emitter. use crate::builtins::spec::BuiltinCheckCtx; @@ -28,7 +29,7 @@ builtin! { params: [callback: Mixed, array: Mixed], variadic: "arrays", min_args: 2, - max_args: 2, + max_args: 3, returns: Mixed, check: check, lower: lower, @@ -40,8 +41,14 @@ builtin! { /// /// Validates that the second argument is an indexed array, checks the callback /// with a dummy element argument, and derives the result element type from the -/// callback return type. Arity (exactly 2 args) is pre-validated by `check_arity`. +/// callback return type. Arity (2 or 3 args) is pre-validated by `check_arity`; the +/// two-input-array form (`array_map($cb, $a, $b)`) defers to `check_array_map_multi`. fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() > 2 { + return crate::types::checker::builtins::check_array_map_multi( + cx.checker, cx.args, cx.span, cx.env, + ); + } for arg in cx.args { cx.checker.infer_type(arg, cx.env)?; } diff --git a/src/builtins/array/array_merge.rs b/src/builtins/array/array_merge.rs index 2fd2b19304..75bdf2ddb3 100644 --- a/src/builtins/array/array_merge.rs +++ b/src/builtins/array/array_merge.rs @@ -6,15 +6,14 @@ //! backend (lower hook), all via `crate::builtins::registry`. //! //! Key details: -//! - The PHP golden signature is `variadic(&[], "arrays")` (min=0). The legacy CHECK -//! arm requires exactly 2 arguments. `min_args: 2, max_args: 2` reproduce that -//! enforcement in `check_arity` only; `function_sig` and the parity gate keep the -//! variadic shape from the golden. -//! - `check` validates that the first argument is an indexed or associative array and -//! returns the merged result type. The return type logic mirrors the legacy checker: -//! when the first operand is an empty array (element type `Void`), the result adopts -//! the second operand's element type if it is a scalar-merge type. -//! - Arity is pre-validated by `check_arity`; the hook can assume exactly 2 args. +//! - The PHP golden signature is `variadic(&[], "arrays")` (min=0). `min_args: 2` enforces +//! at least two arrays in `check_arity`; the variadic shape (unbounded max) accepts three +//! or more, matching PHP. `function_sig` and the parity gate keep the golden shape. +//! - `check` validates that every argument is an indexed or associative array and folds the +//! merged result type left-to-right (`f(f(a, b), c)`), mirroring the ir_lower rewrite. When +//! an operand is an empty array (element type `Void`), the result adopts the next operand's +//! element type if it is a scalar-merge type. +//! - Arity (>= 2 args) is pre-validated by `check_arity`. use crate::builtins::spec::BuiltinCheckCtx; use crate::codegen_ir::context::FunctionContext; @@ -29,7 +28,6 @@ builtin! { params: [], variadic: "arrays", min_args: 2, - max_args: 2, returns: Mixed, check: check, lower: lower, @@ -37,22 +35,30 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.array-merge.php", } -/// Validates the first argument is an array and returns the merged result type. +/// Validates every argument is an array and returns the merged result type. /// -/// Arity (exactly 2 args) is pre-validated by `check_arity`. The hook re-infers both -/// argument types to derive the precise result type: when the left operand is an empty -/// indexed array (element type `Void`), the result adopts the right operand's element -/// type if it is a scalar-merge-compatible type. +/// Arity (>= 2 args) is pre-validated by `check_arity`. The hook infers every argument type and +/// folds them left-to-right through `array_merge_return_type` (mirroring the ir_lower rewrite of +/// `array_merge(a, b, c)` into `array_merge(array_merge(a, b), c)`), so a three-plus-array merge +/// gets the same result type the nested two-array lowering produces. fn check(cx: &mut BuiltinCheckCtx) -> Result { - let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; - let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; - if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { - return Err(CompileError::new( - cx.span, - "array_merge() first argument must be array", - )); + let mut types = Vec::with_capacity(cx.args.len()); + for arg in cx.args { + types.push(cx.checker.infer_type(arg, cx.env)?); } - Ok(array_merge_return_type(ty1, ty2)) + for ty in &types { + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_merge() arguments must be arrays", + )); + } + } + let mut result = types[0].clone(); + for ty in &types[1..] { + result = array_merge_return_type(result, ty.clone()); + } + Ok(result) } /// Lowers an `array_merge` call by delegating to the shared array-merge emitter. @@ -83,10 +89,18 @@ fn is_empty_array_element_type(ty: &PhpType) -> bool { matches!(ty.codegen_repr(), PhpType::Void) } -/// Returns true for element types that the scalar merge runtime helper copies safely. +/// Returns true for element types the merged array can adopt from the next operand when the +/// current accumulator is statically empty. Scalars use the 8-byte merge helper; strings use the +/// dedicated 16-byte `__rt_array_merge_str` helper, so `Str` is included here as well (kept in sync +/// with the ir_lower copy in `src/ir_lower/expr/mod.rs`). fn is_scalar_merge_element_type(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Callable | PhpType::Void + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Callable + | PhpType::Void + | PhpType::Str ) } diff --git a/src/builtins/array/array_search.rs b/src/builtins/array/array_search.rs index 44132f82f9..4c81a1a54d 100644 --- a/src/builtins/array/array_search.rs +++ b/src/builtins/array/array_search.rs @@ -8,12 +8,9 @@ //! Key details: //! - `check` validates the second argument is an array and returns a union of the //! key type and Bool (false on not-found), or Int|Bool for indexed arrays. -//! - The golden signature carries the optional `strict` param (min=2, max=3), but the -//! legacy CHECK arm enforced exactly 2 arguments and the `lower_array_search` emitter -//! only supports 2 args. `max_args: 2` reproduces that exact-2 enforcement in -//! `check_arity` only; `function_sig` and the parity gate keep the full param-derived -//! bounds from the golden. This keeps the clean "takes exactly 2 arguments" checker -//! diagnostic for a 3-arg call instead of an EIR backend error. +//! - The optional `strict` param (min=2, max=3) is accepted: elephc's element comparison is +//! value/byte-exact, so it already yields strict semantics for a needle whose type matches the +//! array element type (the supported case). The emitter evaluates but ignores the flag value. //! - `lower` is a thin wrapper over the shared `arrays::lower_array_search` emitter. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; @@ -27,7 +24,6 @@ builtin! { name: "array_search", area: Array, params: [needle: Mixed, haystack: Mixed, strict: Bool = DefaultSpec::Bool(false)], - max_args: 2, returns: Mixed, check: check, lower: lower, @@ -37,12 +33,14 @@ builtin! { /// Validates haystack is an array and returns the key-or-false union type. /// -/// The registry's `check_arity` handles arity enforcement (capped at 2 by `max_args` -/// to match the legacy CHECK arm). For assoc arrays the return is `key_type | bool`; -/// for indexed arrays it is `int | bool`. +/// Infers all arguments (including the optional `strict` flag). For assoc arrays the return is +/// `key_type | bool`; for indexed arrays it is `int | bool`. fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_type(&cx.args[0], cx.env)?; let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if let Some(strict) = cx.args.get(2) { + cx.checker.infer_type(strict, cx.env)?; + } if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { return Err(CompileError::new( cx.span, diff --git a/src/builtins/array/array_slice.rs b/src/builtins/array/array_slice.rs index 5f45d65bfb..8d2324396d 100644 --- a/src/builtins/array/array_slice.rs +++ b/src/builtins/array/array_slice.rs @@ -10,9 +10,13 @@ //! return type is the (array-or-assoc) input type unchanged; a boxed `Mixed`/`Union` //! input yields `Mixed`. A check hook is required because the return type depends on //! the inferred first-argument type. +//! - A literal `preserve_keys = true` 4th argument keeps the original integer offsets, turning an +//! indexed scalar array into an integer-keyed associative result. The literal-true + scalar-element +//! predicate (`crate::types::array_slice_literal_preserve_keys`) MUST stay identical here, in the +//! EIR return-type inference, and in the emitter — a disagreement is a heap-shape mismatch. //! - The declared signature carries the golden param list (`array`, `offset`, -//! `length`), with `length` optional (default `null`), so the registry's -//! `check_arity` accepts 2 or 3 arguments — matching the legacy CHECK arm. +//! `length`, `preserve_keys`), with `length`/`preserve_keys` optional, so the registry's +//! `check_arity` accepts 2 to 4 arguments. //! - `lower` is a thin wrapper over the shared `arrays::lower_array_slice` emitter. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; @@ -25,7 +29,7 @@ use crate::types::PhpType; builtin! { name: "array_slice", area: Array, - params: [array: Mixed, offset: Mixed, length: Mixed = DefaultSpec::Null], + params: [array: Mixed, offset: Mixed, length: Mixed = DefaultSpec::Null, preserve_keys: Bool = DefaultSpec::Bool(false)], returns: Mixed, check: check, lower: lower, @@ -37,9 +41,10 @@ builtin! { /// /// A slice preserves the input array shape, so the (array-or-assoc) first-argument /// type is returned unchanged; a boxed `Mixed`/`Union` first argument yields `Mixed`. -/// Non-array first arguments are rejected. The first argument is re-inferred here; -/// the registry already inferred every argument once for side effects, and arity -/// (2 or 3) is pre-validated by the registry. +/// A literal `preserve_keys = true` 4th argument over a scalar (int/float/bool) indexed array yields +/// an integer-keyed associative array instead. Non-array first arguments are rejected. The first +/// argument is re-inferred here; the registry already inferred every argument once for side effects, +/// and arity (2 to 4) is pre-validated by the registry. fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { @@ -51,6 +56,24 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { "array_slice() first argument must be array", )); } + // preserve_keys=true keeps the original integer offsets, turning an indexed array into an + // integer-keyed associative result. The literal-true + scalar-element predicate MUST stay + // identical to the EIR return-type and emitter checks — disagreeing on Array-vs-AssocArray is a + // heap-shape mismatch (corruption). Only scalar (int/float/bool) elements are supported. + if crate::types::array_slice_literal_preserve_keys(cx.args) { + if let PhpType::Array(inner) = &ty { + if matches!(**inner, PhpType::Int | PhpType::Float | PhpType::Bool) { + return Ok(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: inner.clone(), + }); + } + return Err(CompileError::new( + cx.span, + "array_slice() with preserve_keys=true is only supported for arrays of int, float, or bool", + )); + } + } Ok(ty) } diff --git a/src/builtins/array/in_array.rs b/src/builtins/array/in_array.rs index 9c542bb4f3..2fc89d97fe 100644 --- a/src/builtins/array/in_array.rs +++ b/src/builtins/array/in_array.rs @@ -7,12 +7,9 @@ //! //! Key details: //! - `check` validates that the second argument is an array and returns `Bool`. -//! - The golden signature carries the optional `strict` param (min=2, max=3), but the -//! legacy CHECK arm enforced exactly 2 arguments and the `lower_in_array` emitter -//! only supports 2 args. `max_args: 2` reproduces that exact-2 enforcement in -//! `check_arity` only; `function_sig` and the parity gate keep the full param-derived -//! bounds from the golden. This keeps the clean "takes exactly 2 arguments" checker -//! diagnostic for a 3-arg call instead of an EIR backend error. +//! - The optional `strict` param (min=2, max=3) is accepted: elephc's element comparison is +//! value/byte-exact, so it already yields strict semantics for a needle whose type matches the +//! array element type (the supported case). The emitter evaluates but ignores the flag value. //! - `lower` is a thin wrapper over the shared `arrays::lower_in_array` emitter. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; @@ -26,7 +23,6 @@ builtin! { name: "in_array", area: Array, params: [needle: Mixed, haystack: Mixed, strict: Bool = DefaultSpec::Bool(false)], - max_args: 2, returns: Bool, check: check, lower: lower, @@ -36,12 +32,14 @@ builtin! { /// Validates that the second argument is an array and returns `Bool`. /// -/// The registry's `check_arity` handles arity enforcement (capped at 2 by `max_args` -/// to match the legacy CHECK arm). This hook validates that `haystack` is an array -/// and returns the `Bool` return type. +/// Infers all arguments (including the optional `strict` flag, so its type/side effects are +/// checked) and validates that `haystack` is an array. Returns the `Bool` return type. fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_type(&cx.args[0], cx.env)?; let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if let Some(strict) = cx.args.get(2) { + cx.checker.infer_type(strict, cx.env)?; + } if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { return Err(CompileError::new( cx.span, diff --git a/src/builtins/math/round.rs b/src/builtins/math/round.rs index 09ed2d4967..1f01bd1155 100644 --- a/src/builtins/math/round.rs +++ b/src/builtins/math/round.rs @@ -1,31 +1,67 @@ //! Purpose: -//! Home of the PHP `round` builtin: its declaration and lowering. +//! Home of the PHP `round` builtin: its declaration, type-check hook, and lowering. //! //! Called from: -//! - The builtin registry (declaration) and the EIR backend (lower hook), -//! both via `crate::builtins::registry`. +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. //! //! Key details: -//! - No `check` hook is needed: `round` is a pure-data builtin whose return type -//! (`Float`) is fully determined by its declaration. -//! - The second parameter `precision` is optional with a default of `0`, matching -//! PHP's `round(num, precision = 0)` signature. The registry enforces 1-2 args. +//! - `round(num, precision = 0, mode = PHP_ROUND_HALF_UP)`: the optional `precision` and +//! `mode` parameters give the registry a 1-3 arg signature. +//! - A `check` hook rejects a statically-known `PHP_ROUND_HALF_DOWN` / `PHP_ROUND_HALF_ODD` +//! mode, which are not yet specialized. `PHP_ROUND_HALF_UP` (the default, ties away from +//! zero) and `PHP_ROUND_HALF_EVEN` (banker's rounding) are supported; the return type is +//! always `Float`. -use crate::builtins::spec::DefaultSpec; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::codegen_ir::context::FunctionContext; use crate::codegen_ir::CodegenIrError; +use crate::errors::CompileError; use crate::ir::Instruction; +use crate::parser::ast::{Expr, ExprKind}; +use crate::types::round_constants::round_mode_value; +use crate::types::PhpType; builtin! { name: "round", area: Math, - params: [num: Float, precision: Int = DefaultSpec::Int(0)], + params: [num: Float, precision: Int = DefaultSpec::Int(0), mode: Int = DefaultSpec::Int(1)], returns: Float, + check: check, lower: lower, summary: "Rounds a float.", php_manual: "https://www.php.net/manual/en/function.round.php", } +/// Resolves a statically-known `round()` mode value from an integer literal or a +/// `PHP_ROUND_HALF_*` constant reference. Returns `None` for a runtime (non-constant) mode. +fn static_round_mode(arg: &Expr) -> Option { + match &arg.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::ConstRef(name) => round_mode_value(name.as_str()), + _ => None, + } +} + +/// Type-checks `round($num, $precision, $mode)`, always returning `Float`. +/// +/// Rejects a statically-known `PHP_ROUND_HALF_DOWN` (2) or `PHP_ROUND_HALF_ODD` (4) mode with a +/// diagnostic, since only `PHP_ROUND_HALF_UP` (1) and `PHP_ROUND_HALF_EVEN` (3) are specialized. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + if let Some(mode) = cx.args.get(2).and_then(static_round_mode) { + if mode == 2 || mode == 4 { + return Err(CompileError::new( + cx.span, + "round(): only PHP_ROUND_HALF_UP and PHP_ROUND_HALF_EVEN modes are supported", + )); + } + } + Ok(PhpType::Float) +} + /// Lowers a `round` call by dispatching to the shared float-rounding emitter. fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { crate::codegen_ir::lower_inst::builtins::math::lower_round(ctx, inst) diff --git a/src/codegen/abi/diagnostics.rs b/src/codegen/abi/diagnostics.rs new file mode 100644 index 0000000000..160f33f7f2 --- /dev/null +++ b/src/codegen/abi/diagnostics.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Emits inline fatal-diagnostic-and-exit sequences shared across emitters. +//! Centralizes the target-specific write(stderr)+exit syscalls so callers do not duplicate them. +//! +//! Called from: +//! - `crate::codegen_ir::lower_inst::arithmetic` (integer `/` and `%` by zero) and +//! `crate::codegen_ir::lower_inst::floats` (float `/` by zero), plus any other EIR emitter +//! that must abort with a message. +//! +//! Key details: +//! - Writes the message to file descriptor 2 (stderr) and terminates with exit code 1, matching +//! elephc's existing builtin fatals (e.g. intdiv by zero). The error is NOT catchable by PHP +//! try/catch — elephc has no runtime-error exception mechanism yet. + +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits a write of `message` to stderr (fd 2) followed by `exit(1)`, for the current target. +/// +/// The message bytes are interned in the data section. Control never returns from this sequence +/// (the process exits), so callers branch here for unrecoverable conditions such as division by +/// zero. The same sequence is emitted on every supported target; only the syscall mechanics differ. +/// +/// # Arguments +/// * `emitter` - assembly emitter (provides target arch and instruction emission) +/// * `data` - data section used to intern the message string +/// * `message` - the bytes written to stderr (include a trailing newline if desired) +pub fn emit_fatal_to_stderr(emitter: &mut Emitter, data: &mut DataSection, message: &[u8]) { + let (err_label, err_len) = data.add_string(message); + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, #2"); // fd = stderr + emitter.adrp("x1", &err_label); // load the page that contains the fatal message + emitter.add_lo12("x1", "x1", &err_label); // resolve the fatal message address within that page + emitter.instruction(&format!("mov x2, #{}", err_len)); // pass the fatal message length to write() + emitter.syscall(4); // write the message to stderr + emitter.instruction("mov x0, #1"); // exit code 1 + emitter.syscall(1); // terminate the process + } + Arch::X86_64 => { + emitter.instruction(&format!("lea rsi, [rip + {}]", err_label)); // point the write() buffer register at the fatal message + emitter.instruction(&format!("mov edx, {}", err_len)); // pass the fatal message length to write() + emitter.instruction("mov edi, 2"); // fd = stderr + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // emit the fatal message before terminating + emitter.instruction("mov edi, 1"); // exit code 1 + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process + } + } +} diff --git a/src/codegen/abi/mod.rs b/src/codegen/abi/mod.rs index 6380cab11d..db4c531273 100644 --- a/src/codegen/abi/mod.rs +++ b/src/codegen/abi/mod.rs @@ -10,6 +10,7 @@ mod bootstrap; mod calls; +mod diagnostics; mod frame; mod registers; mod symbols; @@ -21,6 +22,7 @@ pub use bootstrap::{ emit_copy_frame_pointer, emit_enable_heap_debug_flag, emit_exit, emit_exit_with_result_reg, emit_store_process_args_to_globals, }; +pub use diagnostics::emit_fatal_to_stderr; pub use calls::{ build_outgoing_arg_assignments_for_target, emit_call_label, emit_call_reg, emit_pop_reg, emit_pop_float_reg, emit_pop_reg_pair, emit_push_float_reg, emit_push_reg, diff --git a/src/codegen/prescan.rs b/src/codegen/prescan.rs index d36446c50f..7dd1aa5e3c 100644 --- a/src/codegen/prescan.rs +++ b/src/codegen/prescan.rs @@ -17,6 +17,7 @@ use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; +use crate::types::round_constants::ROUND_INT_CONSTANTS; use crate::types::{PhpType, TypeEnv}; use super::context::{Context, TRY_HANDLER_SLOT_SIZE}; @@ -138,6 +139,12 @@ pub(crate) fn collect_constants( (ExprKind::IntLiteral(*value), PhpType::Int), ); } + for (name, value) in ROUND_INT_CONSTANTS { + constants.insert( + (*name).to_string(), + (ExprKind::IntLiteral(*value), PhpType::Int), + ); + } collect_constant_decls(program, &mut constants); constants } diff --git a/src/codegen/runtime/arrays/array_map2.rs b/src/codegen/runtime/arrays/array_map2.rs new file mode 100644 index 0000000000..dd8221c260 --- /dev/null +++ b/src/codegen/runtime/arrays/array_map2.rs @@ -0,0 +1,194 @@ +//! Purpose: +//! Emits the `__rt_array_map2` runtime helper for the two-input-array form of PHP `array_map`. +//! Zips two integer arrays element-wise through a callback into a new integer-keyed list. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Result length is `max(len(a), len(b))`; the shorter array is padded with 0 (PHP passes null, +//! which coerces to 0 in integer context). +//! - The callback is always invoked as `cb(elem0, elem1, env)`; for a non-capturing callback the +//! environment argument is 0 and ignored, matching the direct-call path emitted by codegen. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_array_map2` runtime helper. +/// +/// Iterates `i` from 0 to `max(len0, len1)`, loading `arr0[i]` and `arr1[i]` (or 0 when the +/// index is past the end of that array), invoking the callback with both elements plus the +/// optional capture environment, and storing each result into a freshly allocated array. +/// +/// # Input registers +/// - ARM64: `x0` = callback/wrapper entry, `x1` = first array, `x2` = second array, `x3` = env (0 = none) +/// - x86_64: `rdi` = callback/wrapper entry, `rsi` = first array, `rdx` = second array, `rcx` = env (0 = none) +/// +/// # Output registers +/// - ARM64: `x0` = new mapped array pointer; x86_64: `rax` = new mapped array pointer +pub fn emit_array_map2(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_map2_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_map2 (two input arrays) ---"); + emitter.label_global("__rt_array_map2"); + + // -- set up stack frame (frame layout documented inline) -- + emitter.instruction("sub sp, sp, #96"); // allocate 96 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish new frame pointer + emitter.instruction("stp x19, x20, [sp, #64]"); // save callee-saved x19, x20 + + // -- save inputs: x19=callback entry, x20=loop index; stack holds env/arrays/lengths/dest -- + emitter.instruction("mov x19, x0"); // x19 = callback entry point (callee-saved across calls) + emitter.instruction("str x3, [sp, #0]"); // save the optional capture environment pointer + emitter.instruction("str x1, [sp, #8]"); // save the first source array pointer + emitter.instruction("str x2, [sp, #16]"); // save the second source array pointer + emitter.instruction("ldr x9, [x1]"); // x9 = length of the first array + emitter.instruction("str x9, [sp, #32]"); // save the first array length + emitter.instruction("ldr x10, [x2]"); // x10 = length of the second array + emitter.instruction("str x10, [sp, #40]"); // save the second array length + + // -- compute result length = max(len0, len1) -- + emitter.instruction("cmp x9, x10"); // compare the two array lengths + emitter.instruction("csel x11, x9, x10, ge"); // x11 = max(len0, len1) + emitter.instruction("str x11, [sp, #48]"); // save the result length + + // -- allocate the destination array with 8-byte integer slots -- + emitter.instruction("mov x0, x11"); // x0 = capacity for the new array + emitter.instruction("mov x1, #8"); // x1 = element size (8 bytes for int) + emitter.instruction("bl __rt_array_new"); // allocate new array → x0 = new array pointer + emitter.instruction("str x0, [sp, #24]"); // save the destination array pointer + emitter.instruction("mov x20, #0"); // x20 = loop index i = 0 + + // -- loop: zip arr0[i] and arr1[i] through the callback -- + emitter.label("__rt_array_map2_loop"); + emitter.instruction("ldr x11, [sp, #48]"); // reload the result length + emitter.instruction("cmp x20, x11"); // compare i with the result length + emitter.instruction("b.ge __rt_array_map2_done"); // if i >= result length, loop complete + + // -- load arr0[i] into x0, or 0 when i is past the first array's end -- + emitter.instruction("ldr x9, [sp, #32]"); // reload the first array length + emitter.instruction("cmp x20, x9"); // is i within the first array's bounds? + emitter.instruction("b.ge __rt_array_map2_elem0_zero"); // out of bounds → pad with 0 + emitter.instruction("ldr x1, [sp, #8]"); // reload the first array pointer + emitter.instruction("add x1, x1, #24"); // skip the array header to the data region + emitter.instruction("ldr x0, [x1, x20, lsl #3]"); // x0 = arr0[i] + emitter.instruction("b __rt_array_map2_elem0_ready"); // first element is ready + emitter.label("__rt_array_map2_elem0_zero"); + emitter.instruction("mov x0, #0"); // pad the first element with 0 (PHP null in int context) + emitter.label("__rt_array_map2_elem0_ready"); + + // -- load arr1[i] into x1, or 0 when i is past the second array's end (must not touch x0) -- + emitter.instruction("ldr x10, [sp, #40]"); // reload the second array length + emitter.instruction("cmp x20, x10"); // is i within the second array's bounds? + emitter.instruction("b.ge __rt_array_map2_elem1_zero"); // out of bounds → pad with 0 + emitter.instruction("ldr x2, [sp, #16]"); // reload the second array pointer + emitter.instruction("add x2, x2, #24"); // skip the array header to the data region + emitter.instruction("ldr x1, [x2, x20, lsl #3]"); // x1 = arr1[i] + emitter.instruction("b __rt_array_map2_elem1_ready"); // second element is ready + emitter.label("__rt_array_map2_elem1_zero"); + emitter.instruction("mov x1, #0"); // pad the second element with 0 (PHP null in int context) + emitter.label("__rt_array_map2_elem1_ready"); + + // -- pass the capture environment and call the callback -- + emitter.instruction("ldr x2, [sp, #0]"); // x2 = capture environment (0 for non-capturing callbacks) + emitter.instruction("blr x19"); // call cb(elem0, elem1, env) → result in x0 + + // -- store the result into the destination array -- + emitter.instruction("ldr x1, [sp, #24]"); // reload the destination array pointer + emitter.instruction("add x2, x1, #24"); // skip the array header to the data region + emitter.instruction("str x0, [x2, x20, lsl #3]"); // dest[i] = callback result + emitter.instruction("add x20, x20, #1"); // i += 1 + emitter.instruction("b __rt_array_map2_loop"); // continue the zip loop + + // -- publish the destination length and return -- + emitter.label("__rt_array_map2_done"); + emitter.instruction("ldr x0, [sp, #24]"); // x0 = destination array pointer + emitter.instruction("ldr x9, [sp, #48]"); // x9 = result length + emitter.instruction("str x9, [x0]"); // publish the destination array length + emitter.instruction("ldp x19, x20, [sp, #64]"); // restore callee-saved x19, x20 + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // deallocate stack frame + emitter.instruction("ret"); // return with x0 = new mapped array +} + +/// Emits the x86_64 (System V) implementation of `__rt_array_map2`. +/// +/// Same behavior as the ARM64 variant. The callback receives the two elements in `rdi`/`rsi` +/// and the capture environment in `rdx`; the transformed result is read from `rax`. +fn emit_array_map2_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_map2 (two input arrays) ---"); + emitter.label_global("__rt_array_map2"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving array-map2 spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the callback, source arrays, and destination slots + emitter.instruction("push r12"); // preserve the callback scratch register across every callback invocation + emitter.instruction("push r13"); // preserve the loop-index scratch register across callback invocations + emitter.instruction("sub rsp, 80"); // reserve local slots; 80 keeps rsp 16-byte aligned before the SysV calls below + emitter.instruction("mov r12, rdi"); // keep the callback entry point in a callee-saved register across the zip loop + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the optional capture environment pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rsi"); // save the first source array pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the second source array pointer + emitter.instruction("mov rax, QWORD PTR [rsi]"); // load the length of the first array + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the first array length + emitter.instruction("mov r10, QWORD PTR [rdx]"); // load the length of the second array + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // save the second array length + emitter.instruction("cmp rax, r10"); // compare the two array lengths + emitter.instruction("cmovl rax, r10"); // rax = max(len0, len1) + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save the result length + emitter.instruction("mov rdi, rax"); // pass the result length as the destination capacity + emitter.instruction("mov rsi, 8"); // request 8-byte integer element slots + emitter.instruction("call __rt_array_new"); // allocate the destination array → rax = new array pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the destination array pointer + emitter.instruction("xor r13d, r13d"); // start the zip loop at logical index zero + + emitter.label("__rt_array_map2_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload the result length + emitter.instruction("cmp r13, rax"); // compare i with the result length + emitter.instruction("jge __rt_array_map2_done"); // exit once every result slot has been produced + + // -- load arr0[i] into rdi, or 0 when i is past the first array's end -- + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the first array length + emitter.instruction("cmp r13, rax"); // is i within the first array's bounds? + emitter.instruction("jge __rt_array_map2_elem0_zero"); // out of bounds → pad with 0 + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the first array pointer + emitter.instruction("mov rdi, QWORD PTR [r10 + r13 * 8 + 24]"); // rdi = arr0[i] + emitter.instruction("jmp __rt_array_map2_elem0_ready"); // first element is ready + emitter.label("__rt_array_map2_elem0_zero"); + emitter.instruction("xor edi, edi"); // pad the first element with 0 (PHP null in int context) + emitter.label("__rt_array_map2_elem0_ready"); + + // -- load arr1[i] into rsi, or 0 when i is past the second array's end (must not touch rdi) -- + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the second array length + emitter.instruction("cmp r13, rax"); // is i within the second array's bounds? + emitter.instruction("jge __rt_array_map2_elem1_zero"); // out of bounds → pad with 0 + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the second array pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + r13 * 8 + 24]"); // rsi = arr1[i] + emitter.instruction("jmp __rt_array_map2_elem1_ready"); // second element is ready + emitter.label("__rt_array_map2_elem1_zero"); + emitter.instruction("xor esi, esi"); // pad the second element with 0 (PHP null in int context) + emitter.label("__rt_array_map2_elem1_ready"); + + // -- pass the capture environment and call the callback -- + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // rdx = capture environment (0 for non-capturing callbacks) + emitter.instruction("call r12"); // call cb(elem0, elem1, env) → result in rax + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the destination array pointer after the callback + emitter.instruction("mov QWORD PTR [r10 + r13 * 8 + 24], rax"); // dest[i] = callback result + emitter.instruction("add r13, 1"); // i += 1 + emitter.instruction("jmp __rt_array_map2_loop"); // continue the zip loop + + emitter.label("__rt_array_map2_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the destination array pointer for the return value + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the result length + emitter.instruction("mov QWORD PTR [rax], r10"); // publish the destination array length + emitter.instruction("add rsp, 80"); // release the local bookkeeping slots + emitter.instruction("pop r13"); // restore the caller's loop-index callee-saved register + emitter.instruction("pop r12"); // restore the caller's callback callee-saved register + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return rax = new mapped array pointer +} diff --git a/src/codegen/runtime/arrays/array_map2_str.rs b/src/codegen/runtime/arrays/array_map2_str.rs new file mode 100644 index 0000000000..34690f5179 --- /dev/null +++ b/src/codegen/runtime/arrays/array_map2_str.rs @@ -0,0 +1,210 @@ +//! Purpose: +//! Emits the `__rt_array_map2_str` runtime helper for the two-input-array form of PHP `array_map` +//! over string arrays. Zips two string arrays element-wise through a string-returning callback. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Source elements are 16-byte (pointer/length) string slots; each element is passed to the +//! callback as a string argument (`x0`/`x1` and `x2`/`x3` on AArch64; `rdi`/`rsi` and `rdx`/`rcx` +//! on x86_64). The capture environment is the callback's fifth integer argument (`x4`/`r8`). +//! - Result length is `max(len(a), len(b))`; the shorter array is padded with the empty string +//! (pointer 0, length 0 — PHP passes null, which is "" in string context). +//! - Each callback result is persisted and appended through `__rt_array_push_str`, which owns the +//! copy and grows the destination as needed. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_array_map2_str` runtime helper. +/// +/// # Input registers +/// - ARM64: `x0` = callback entry, `x1` = first array, `x2` = second array, `x3` = env (0 = none) +/// - x86_64: `rdi` = callback entry, `rsi` = first array, `rdx` = second array, `rcx` = env (0 = none) +/// +/// # Output registers +/// - ARM64: `x0` = new mapped string array pointer; x86_64: `rax` = new mapped string array pointer +pub fn emit_array_map2_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_map2_str_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_map2_str (two string input arrays) ---"); + emitter.label_global("__rt_array_map2_str"); + + // -- set up stack frame -- + emitter.instruction("sub sp, sp, #96"); // allocate 96 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish new frame pointer + emitter.instruction("stp x19, x20, [sp, #64]"); // save callee-saved x19, x20 + + // -- save inputs -- + emitter.instruction("mov x19, x0"); // x19 = callback entry point (callee-saved across calls) + emitter.instruction("str x3, [sp, #0]"); // save the optional capture environment pointer + emitter.instruction("str x1, [sp, #8]"); // save the first source array pointer + emitter.instruction("str x2, [sp, #16]"); // save the second source array pointer + emitter.instruction("ldr x9, [x1]"); // x9 = length of the first array + emitter.instruction("str x9, [sp, #32]"); // save the first array length + emitter.instruction("ldr x10, [x2]"); // x10 = length of the second array + emitter.instruction("str x10, [sp, #40]"); // save the second array length + + // -- compute result length = max(len0, len1) -- + emitter.instruction("cmp x9, x10"); // compare the two array lengths + emitter.instruction("csel x11, x9, x10, ge"); // x11 = max(len0, len1) + emitter.instruction("str x11, [sp, #48]"); // save the result length + + // -- allocate the destination array with 16-byte string slots (length 0, push appends) -- + emitter.instruction("mov x0, x11"); // x0 = capacity for the new array + emitter.instruction("mov x1, #16"); // x1 = element size (16 bytes for a string slot) + emitter.instruction("bl __rt_array_new"); // allocate new array → x0 = new array pointer + emitter.instruction("str x0, [sp, #24]"); // save the destination array pointer + emitter.instruction("mov x20, #0"); // x20 = loop index i = 0 + + // -- loop: zip arr0[i] and arr1[i] through the callback -- + emitter.label("__rt_array_map2_str_loop"); + emitter.instruction("ldr x11, [sp, #48]"); // reload the result length + emitter.instruction("cmp x20, x11"); // compare i with the result length + emitter.instruction("b.ge __rt_array_map2_str_done"); // if i >= result length, loop complete + + // -- load arr0[i] into x0/x1 (ptr/len), or the empty string when past the first array's end -- + emitter.instruction("ldr x9, [sp, #32]"); // reload the first array length + emitter.instruction("cmp x20, x9"); // is i within the first array's bounds? + emitter.instruction("b.ge __rt_array_map2_str_elem0_empty"); // out of bounds → pad with the empty string + emitter.instruction("ldr x10, [sp, #8]"); // reload the first array pointer + emitter.instruction("add x10, x10, #24"); // skip the array header to the data region + emitter.instruction("lsl x11, x20, #4"); // compute the 16-byte string-slot offset for index i + emitter.instruction("add x10, x10, x11"); // compute the address of the current first-array string slot + emitter.instruction("ldr x0, [x10]"); // x0 = arr0[i] string pointer + emitter.instruction("ldr x1, [x10, #8]"); // x1 = arr0[i] string length + emitter.instruction("b __rt_array_map2_str_elem0_ready"); // first element is ready + emitter.label("__rt_array_map2_str_elem0_empty"); + emitter.instruction("mov x0, #0"); // empty-string pointer for the missing first element + emitter.instruction("mov x1, #0"); // empty-string length for the missing first element + emitter.label("__rt_array_map2_str_elem0_ready"); + + // -- load arr1[i] into x2/x3, or the empty string (must not touch x0/x1) -- + emitter.instruction("ldr x9, [sp, #40]"); // reload the second array length + emitter.instruction("cmp x20, x9"); // is i within the second array's bounds? + emitter.instruction("b.ge __rt_array_map2_str_elem1_empty"); // out of bounds → pad with the empty string + emitter.instruction("ldr x10, [sp, #16]"); // reload the second array pointer + emitter.instruction("add x10, x10, #24"); // skip the array header to the data region + emitter.instruction("lsl x11, x20, #4"); // compute the 16-byte string-slot offset for index i + emitter.instruction("add x10, x10, x11"); // compute the address of the current second-array string slot + emitter.instruction("ldr x2, [x10]"); // x2 = arr1[i] string pointer + emitter.instruction("ldr x3, [x10, #8]"); // x3 = arr1[i] string length + emitter.instruction("b __rt_array_map2_str_elem1_ready"); // second element is ready + emitter.label("__rt_array_map2_str_elem1_empty"); + emitter.instruction("mov x2, #0"); // empty-string pointer for the missing second element + emitter.instruction("mov x3, #0"); // empty-string length for the missing second element + emitter.label("__rt_array_map2_str_elem1_ready"); + + // -- pass the capture environment and call the callback -- + emitter.instruction("ldr x4, [sp, #0]"); // x4 = capture environment (0 for non-capturing callbacks) + emitter.instruction("blr x19"); // call cb(s0, s1, env) → string result in x1=ptr, x2=len + + // -- persist and append the result string into the destination array -- + emitter.instruction("ldr x0, [sp, #24]"); // reload the destination array pointer for the append + emitter.instruction("bl __rt_array_push_str"); // persist + append the result string, returning the possibly-grown array in x0 + emitter.instruction("str x0, [sp, #24]"); // store the possibly-grown destination array pointer back + emitter.instruction("add x20, x20, #1"); // i += 1 + emitter.instruction("b __rt_array_map2_str_loop"); // continue the zip loop + + // -- return the destination array (its length was published by __rt_array_push_str) -- + emitter.label("__rt_array_map2_str_done"); + emitter.instruction("ldr x0, [sp, #24]"); // x0 = destination array pointer + emitter.instruction("ldp x19, x20, [sp, #64]"); // restore callee-saved x19, x20 + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // deallocate stack frame + emitter.instruction("ret"); // return with x0 = new mapped string array +} + +/// Emits the x86_64 (System V) implementation of `__rt_array_map2_str`. +/// +/// Same behavior as the ARM64 variant. The two string elements are passed in `rdi`/`rsi` and +/// `rdx`/`rcx`, the capture environment in `r8`; the callback returns the result string in +/// `rax`/`rdx`, which is appended via `__rt_array_push_str`. +fn emit_array_map2_str_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_map2_str (two string input arrays) ---"); + emitter.label_global("__rt_array_map2_str"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the callback, source arrays, and destination slots + emitter.instruction("push r12"); // preserve the callback scratch register across every callback invocation + emitter.instruction("push r13"); // preserve the loop-index scratch register across callback invocations + emitter.instruction("sub rsp, 80"); // reserve local slots; 80 keeps rsp 16-byte aligned before the SysV calls below + emitter.instruction("mov r12, rdi"); // keep the callback entry point in a callee-saved register across the zip loop + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the optional capture environment pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rsi"); // save the first source array pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the second source array pointer + emitter.instruction("mov rax, QWORD PTR [rsi]"); // load the length of the first array + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the first array length + emitter.instruction("mov r10, QWORD PTR [rdx]"); // load the length of the second array + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // save the second array length + emitter.instruction("cmp rax, r10"); // compare the two array lengths + emitter.instruction("cmovl rax, r10"); // rax = max(len0, len1) + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save the result length + emitter.instruction("mov rdi, rax"); // pass the result length as the destination capacity + emitter.instruction("mov rsi, 16"); // request 16-byte string element slots + emitter.instruction("call __rt_array_new"); // allocate the destination array (length 0; push appends) → rax = new array pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the destination array pointer + emitter.instruction("xor r13d, r13d"); // start the zip loop at logical index zero + + emitter.label("__rt_array_map2_str_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload the result length + emitter.instruction("cmp r13, rax"); // compare i with the result length + emitter.instruction("jge __rt_array_map2_str_done"); // exit once every result slot has been produced + + // -- load arr0[i] into rdi/rsi, or the empty string when past the first array's end -- + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the first array length + emitter.instruction("cmp r13, rax"); // is i within the first array's bounds? + emitter.instruction("jge __rt_array_map2_str_elem0_empty"); // out of bounds → pad with the empty string + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the first array pointer + emitter.instruction("mov rax, r13"); // copy the logical index before scaling to a 16-byte string-slot offset + emitter.instruction("shl rax, 4"); // rax = i * 16 (x86 address scales max out at 8, so scale manually) + emitter.instruction("mov rdi, QWORD PTR [r10 + rax + 24]"); // rdi = arr0[i] string pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + rax + 32]"); // rsi = arr0[i] string length + emitter.instruction("jmp __rt_array_map2_str_elem0_ready"); // first element is ready + emitter.label("__rt_array_map2_str_elem0_empty"); + emitter.instruction("xor edi, edi"); // empty-string pointer for the missing first element + emitter.instruction("xor esi, esi"); // empty-string length for the missing first element + emitter.label("__rt_array_map2_str_elem0_ready"); + + // -- load arr1[i] into rdx/rcx, or the empty string (must not touch rdi/rsi) -- + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the second array length + emitter.instruction("cmp r13, rax"); // is i within the second array's bounds? + emitter.instruction("jge __rt_array_map2_str_elem1_empty"); // out of bounds → pad with the empty string + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the second array pointer + emitter.instruction("mov rax, r13"); // copy the logical index before scaling to a 16-byte string-slot offset + emitter.instruction("shl rax, 4"); // rax = i * 16 (x86 address scales max out at 8, so scale manually) + emitter.instruction("mov rdx, QWORD PTR [r10 + rax + 24]"); // rdx = arr1[i] string pointer + emitter.instruction("mov rcx, QWORD PTR [r10 + rax + 32]"); // rcx = arr1[i] string length + emitter.instruction("jmp __rt_array_map2_str_elem1_ready"); // second element is ready + emitter.label("__rt_array_map2_str_elem1_empty"); + emitter.instruction("xor edx, edx"); // empty-string pointer for the missing second element + emitter.instruction("xor ecx, ecx"); // empty-string length for the missing second element + emitter.label("__rt_array_map2_str_elem1_ready"); + + // -- pass the capture environment and call the callback -- + emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // r8 = capture environment (0 for non-capturing callbacks) + emitter.instruction("call r12"); // call cb(s0, s1, env) → string result in rax=ptr, rdx=len + + // -- persist and append the result string into the destination array -- + emitter.instruction("mov rsi, rax"); // move the result string pointer into the array-push payload register + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the destination array pointer into the array-push receiver register + emitter.instruction("call __rt_array_push_str"); // persist + append the result string, returning the possibly-grown array in rax + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // store the possibly-grown destination array pointer back + emitter.instruction("add r13, 1"); // i += 1 + emitter.instruction("jmp __rt_array_map2_str_loop"); // continue the zip loop + + emitter.label("__rt_array_map2_str_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the destination array pointer for the return value + emitter.instruction("add rsp, 80"); // release the local bookkeeping slots + emitter.instruction("pop r13"); // restore the caller's loop-index callee-saved register + emitter.instruction("pop r12"); // restore the caller's callback callee-saved register + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return rax = new mapped string array pointer +} diff --git a/src/codegen/runtime/arrays/array_merge_str.rs b/src/codegen/runtime/arrays/array_merge_str.rs new file mode 100644 index 0000000000..131427d921 --- /dev/null +++ b/src/codegen/runtime/arrays/array_merge_str.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Emits the `__rt_array_merge_str` runtime helper: merges two indexed STRING arrays +//! (16-byte ptr+len slots) into a new owned string array. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - String indexed arrays use 16-byte (pointer, length) element slots, unlike the +//! scalar/refcounted merges which assume 8-byte payloads. Routing `Array(Str)` through +//! the 8-byte `__rt_array_merge` corrupts every element past the first. +//! - Each element is appended with `__rt_array_push_str`, which persists the string to the +//! heap, so the merged array owns its own copies and is safe to deep-free. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits `__rt_array_merge_str`. +/// Input: x0/rdi = first string array, x1/rsi = second string array. +/// Output: x0/rax = pointer to the new merged string array. +pub fn emit_array_merge_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_merge_str_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_merge_str ---"); + emitter.label_global("__rt_array_merge_str"); + + // -- set up stack frame, save both source string arrays and their lengths -- + emitter.instruction("sub sp, sp, #64"); // allocate stack frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up new frame pointer + emitter.instruction("str x0, [sp, #0]"); // save first array pointer + emitter.instruction("str x1, [sp, #8]"); // save second array pointer + emitter.instruction("ldr x9, [x0]"); // load first array length + emitter.instruction("str x9, [sp, #16]"); // save first array length + emitter.instruction("ldr x10, [x1]"); // load second array length + emitter.instruction("str x10, [sp, #24]"); // save second array length + + // -- allocate an empty destination sized for both arrays (16-byte string slots) -- + emitter.instruction("add x0, x9, x10"); // combined element capacity + emitter.instruction("mov x1, #16"); // 16-byte ptr+len slots for string payloads + emitter.instruction("bl __rt_array_new"); // allocate destination string array + emitter.instruction("str x0, [sp, #32]"); // save destination array pointer + + // -- append every string from the first array (push_str persists each one) -- + emitter.instruction("mov x4, #0"); // first-array loop index + emitter.label("__rt_array_merge_str_copy1"); + emitter.instruction("ldr x9, [sp, #16]"); // reload first array length + emitter.instruction("cmp x4, x9"); // index reached the first array length? + emitter.instruction("b.ge __rt_array_merge_str_copy2_setup"); // move on to the second array + emitter.instruction("ldr x1, [sp, #0]"); // reload first array pointer + emitter.instruction("add x1, x1, #24"); // first array data base + emitter.instruction("lsl x5, x4, #4"); // index × 16-byte string slot + emitter.instruction("add x5, x1, x5"); // address of the current string slot + emitter.instruction("ldr x1, [x5]"); // string pointer → push_str argument + emitter.instruction("ldr x2, [x5, #8]"); // string length → push_str argument + emitter.instruction("str x4, [sp, #40]"); // preserve the loop index across the push_str call + emitter.instruction("ldr x0, [sp, #32]"); // reload destination array pointer + emitter.instruction("bl __rt_array_push_str"); // persist + append the string → x0 = possibly-grown destination + emitter.instruction("str x0, [sp, #32]"); // save the possibly-grown destination pointer + emitter.instruction("ldr x4, [sp, #40]"); // restore the loop index + emitter.instruction("add x4, x4, #1"); // advance the first-array loop index + emitter.instruction("b __rt_array_merge_str_copy1"); // continue with the first array + + // -- append every string from the second array after the first segment -- + emitter.label("__rt_array_merge_str_copy2_setup"); + emitter.instruction("mov x4, #0"); // second-array loop index + emitter.label("__rt_array_merge_str_copy2"); + emitter.instruction("ldr x10, [sp, #24]"); // reload second array length + emitter.instruction("cmp x4, x10"); // index reached the second array length? + emitter.instruction("b.ge __rt_array_merge_str_done"); // finished merging + emitter.instruction("ldr x1, [sp, #8]"); // reload second array pointer + emitter.instruction("add x1, x1, #24"); // second array data base + emitter.instruction("lsl x5, x4, #4"); // index × 16-byte string slot + emitter.instruction("add x5, x1, x5"); // address of the current string slot + emitter.instruction("ldr x1, [x5]"); // string pointer → push_str argument + emitter.instruction("ldr x2, [x5, #8]"); // string length → push_str argument + emitter.instruction("str x4, [sp, #40]"); // preserve the loop index across the push_str call + emitter.instruction("ldr x0, [sp, #32]"); // reload destination array pointer + emitter.instruction("bl __rt_array_push_str"); // persist + append the string → x0 = possibly-grown destination + emitter.instruction("str x0, [sp, #32]"); // save the possibly-grown destination pointer + emitter.instruction("ldr x4, [sp, #40]"); // restore the loop index + emitter.instruction("add x4, x4, #1"); // advance the second-array loop index + emitter.instruction("b __rt_array_merge_str_copy2"); // continue with the second array + + // -- return the merged string array (length was set incrementally by push_str) -- + emitter.label("__rt_array_merge_str_done"); + emitter.instruction("ldr x0, [sp, #32]"); // reload the merged destination array pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.instruction("ret"); // return merged array in x0 +} + +/// x86_64 Linux variant of `__rt_array_merge_str`. +/// Takes first/second string-array pointers in rdi/rsi, returns the merged array in rax. +/// Mirrors the ARM64 algorithm using `__rt_array_push_str` to persist and append each string. +fn emit_array_merge_str_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_merge_str ---"); + emitter.label_global("__rt_array_merge_str"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving merge spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source arrays, lengths, destination, and index + emitter.instruction("sub rsp, 48"); // reserve aligned spill slots while keeping helper calls 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the first source string-array pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the second source string-array pointer + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first source string-array length + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the first source string-array length + emitter.instruction("mov r11, QWORD PTR [rsi]"); // load the second source string-array length + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the second source string-array length + emitter.instruction("mov rdi, r10"); // seed the combined capacity from the first length + emitter.instruction("add rdi, r11"); // add the second length for the combined capacity + emitter.instruction("mov rsi, 16"); // request 16-byte ptr+len slots for string payloads + emitter.instruction("call __rt_array_new"); // allocate the destination string array + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the destination string-array pointer + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // initialize the first-array loop index + + emitter.label("__rt_array_merge_str_copy_first_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the first-array loop index + emitter.instruction("cmp rcx, QWORD PTR [rbp - 24]"); // index reached the first array length? + emitter.instruction("jge __rt_array_merge_str_copy_second_setup_x86"); // move on to the second array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the first source string-array pointer + emitter.instruction("lea r10, [r10 + 24]"); // first array data base + emitter.instruction("mov r11, rcx"); // copy the loop index for slot scaling + emitter.instruction("shl r11, 4"); // index × 16-byte string slot + emitter.instruction("mov rsi, QWORD PTR [r10 + r11]"); // string pointer → push_str argument + emitter.instruction("mov rdx, QWORD PTR [r10 + r11 + 8]"); // string length → push_str argument + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the destination array pointer + emitter.instruction("call __rt_array_push_str"); // persist + append the string → rax = possibly-grown destination + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the possibly-grown destination pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the loop index after the call + emitter.instruction("add rcx, 1"); // advance the first-array loop index + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // persist the updated index + emitter.instruction("jmp __rt_array_merge_str_copy_first_x86"); // continue with the first array + + emitter.label("__rt_array_merge_str_copy_second_setup_x86"); + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // reset the loop index for the second array + + emitter.label("__rt_array_merge_str_copy_second_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the second-array loop index + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // index reached the second array length? + emitter.instruction("jge __rt_array_merge_str_done_x86"); // finished merging + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the second source string-array pointer + emitter.instruction("lea r10, [r10 + 24]"); // second array data base + emitter.instruction("mov r11, rcx"); // copy the loop index for slot scaling + emitter.instruction("shl r11, 4"); // index × 16-byte string slot + emitter.instruction("mov rsi, QWORD PTR [r10 + r11]"); // string pointer → push_str argument + emitter.instruction("mov rdx, QWORD PTR [r10 + r11 + 8]"); // string length → push_str argument + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the destination array pointer + emitter.instruction("call __rt_array_push_str"); // persist + append the string → rax = possibly-grown destination + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the possibly-grown destination pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the loop index after the call + emitter.instruction("add rcx, 1"); // advance the second-array loop index + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // persist the updated index + emitter.instruction("jmp __rt_array_merge_str_copy_second_x86"); // continue with the second array + + emitter.label("__rt_array_merge_str_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the merged destination string-array pointer + emitter.instruction("add rsp, 48"); // release the merge spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return merged array in rax +} diff --git a/src/codegen/runtime/arrays/array_product_float.rs b/src/codegen/runtime/arrays/array_product_float.rs new file mode 100644 index 0000000000..d9c48a8a19 --- /dev/null +++ b/src/codegen/runtime/arrays/array_product_float.rs @@ -0,0 +1,84 @@ +//! Purpose: +//! Emits the `__rt_array_product_float` runtime helper assembly for floating-point array product. +//! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Used when `array_product()` is applied to a `float[]`. Slots hold raw IEEE doubles, so the product +//! must accumulate with floating-point multiplies (`fmul`/`mulsd`) seeded with 1.0 and return in the +//! float result register (`d0`/`xmm0`), unlike the integer `__rt_array_product`. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_array_product_float` runtime helper for multiplying all double elements of an indexed array. +/// +/// Dispatches to the x86_64 Linux variant if the target is `Arch::X86_64`; otherwise emits an ARM64 +/// implementation. Both assume the array pointer is in `x0`/`rdi` and return the product in the float +/// result register `d0`/`xmm0`. The helper skips the 24-byte array header and iterates over 8-byte +/// double payload slots, accumulating a floating-point product seeded with 1.0 (so an empty array +/// returns 1.0, matching PHP's multiplicative identity). +/// +/// # Arguments +/// * `emitter` - the assembly emitter to write instructions into +pub fn emit_array_product_float(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_product_float_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_product_float ---"); + emitter.label_global("__rt_array_product_float"); + + // -- set up loop variables -- + emitter.instruction("ldr x9, [x0]"); // x9 = array length from header + emitter.instruction("add x10, x0, #24"); // x10 = base of data region (skip 24-byte header) + emitter.instruction("mov x11, #0"); // x11 = i = 0 (loop counter) + emitter.instruction("fmov d0, #1.0"); // d0 = accumulator = 1.0 (multiplicative identity) + + // -- iterate and accumulate floating-point product -- + emitter.label("__rt_array_product_float_loop"); + emitter.instruction("cmp x11, x9"); // compare i with array length + emitter.instruction("b.ge __rt_array_product_float_done"); // if i >= length, we're done + emitter.instruction("ldr d1, [x10, x11, lsl #3]"); // d1 = data[i] as a double + emitter.instruction("fmul d0, d0, d1"); // accumulator *= data[i] (floating-point multiply) + emitter.instruction("add x11, x11, #1"); // i += 1 + emitter.instruction("b __rt_array_product_float_loop"); // continue loop + + // -- return the product (already in d0) -- + emitter.label("__rt_array_product_float_done"); + emitter.instruction("ret"); // return to caller with product in d0 +} + +/// Emits the x86_64 Linux implementation of `__rt_array_product_float` using the System V AMD64 ABI. +/// +/// Uses `rdi` for the array pointer, `xmm0` for the accumulator/return value, `rcx` as the loop +/// cursor, `r10` for the array length, and `r11` for the data region base. Seeds `xmm0` with 1.0 via +/// its bit pattern, then multiplies 8-byte double payloads at `rdi + 24 + rcx * 8` with `mulsd`. +/// +/// # Arguments +/// * `emitter` - the assembly emitter to write x86_64 instructions into +fn emit_array_product_float_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_product_float ---"); + emitter.label_global("__rt_array_product_float"); + + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before starting the double product loop + emitter.instruction("lea r11, [rdi + 24]"); // compute the first double payload slot address in the source indexed array + emitter.instruction("xor ecx, ecx"); // initialize the double product loop cursor at the front of the source indexed array + emitter.instruction("mov rax, 0x3FF0000000000000"); // load the IEEE-754 bit pattern for 1.0 to seed the multiplicative identity + emitter.instruction("movq xmm0, rax"); // seed the double product accumulator with 1.0 before visiting any source payloads + + emitter.label("__rt_array_product_float_loop_x86"); + emitter.instruction("cmp rcx, r10"); // compare the double product loop cursor against the source indexed-array logical length + emitter.instruction("jge __rt_array_product_float_done_x86"); // finish once every double payload has contributed to the product accumulator + emitter.instruction("mulsd xmm0, QWORD PTR [r11 + rcx * 8]"); // multiply the running double product accumulator by the current source payload + emitter.instruction("add rcx, 1"); // advance the double product loop cursor after consuming one source payload + emitter.instruction("jmp __rt_array_product_float_loop_x86"); // continue multiplying source double payloads until the source array is exhausted + + emitter.label("__rt_array_product_float_done_x86"); + emitter.instruction("ret"); // return the double product accumulator in xmm0 +} diff --git a/src/codegen/runtime/arrays/array_slice_preserve.rs b/src/codegen/runtime/arrays/array_slice_preserve.rs new file mode 100644 index 0000000000..20dcbb4ffd --- /dev/null +++ b/src/codegen/runtime/arrays/array_slice_preserve.rs @@ -0,0 +1,216 @@ +//! Purpose: +//! Emits the `__rt_array_slice_preserve` runtime helper assembly for `array_slice($a, $off, $len, true)`. +//! Builds an integer-keyed associative result that preserves the source indexed array's original keys. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Mirrors `__rt_array_slice`'s offset/length normalization, then builds a hash like +//! `__rt_array_flip` (integer keys via the `key_hi == -1` sentinel) where each element keeps its +//! original index `offset + i` as the key. Scalar (8-byte) values only; the source element tag is +//! carried through so the boxed hash values read back as the correct type. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_array_slice_preserve` runtime helper. +/// +/// Input: x0 = source indexed array, x1 = offset, x2 = length (-1 = until end), x3 = value_type tag +/// Output: x0 = new hash table keyed by the preserved original integer indices +pub fn emit_array_slice_preserve(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_slice_preserve_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_slice_preserve ---"); + emitter.label_global("__rt_array_slice_preserve"); + + // Stack layout: + // [sp, #0] = source array pointer + // [sp, #8] = value_type tag + // [sp, #16] = normalized offset + // [sp, #24] = clamped slice length + // [sp, #32] = result hash pointer + // [sp, #40] = loop index i + // [sp, #48] = saved x29 + // [sp, #56] = saved x30 + emitter.instruction("sub sp, sp, #64"); // allocate the slice-preserve stack frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up new frame pointer + emitter.instruction("str x0, [sp, #0]"); // save source array pointer + emitter.instruction("str x3, [sp, #8]"); // save the source element value_type tag + emitter.instruction("ldr x9, [x0]"); // x9 = source array length + + // -- normalize a negative offset to a positive index (clamped to 0) -- + emitter.instruction("cmp x1, #0"); // is the requested offset negative? + emitter.instruction("b.ge __rt_aslp_pos_off"); // skip adjustment when the offset is already non-negative + emitter.instruction("add x1, x9, x1"); // offset = length + offset for negative offsets + emitter.instruction("cmp x1, #0"); // is the adjusted offset still before the start? + emitter.instruction("csel x1, xzr, x1, lt"); // clamp a still-negative offset to zero + + // -- clamp the slice length to the available tail -- + emitter.label("__rt_aslp_pos_off"); + emitter.instruction("cmp x1, x9"); // does the offset start at or beyond the array length? + emitter.instruction("b.ge __rt_aslp_empty"); // an out-of-range offset yields an empty result hash + emitter.instruction("sub x3, x9, x1"); // x3 = remaining = length - offset + emitter.instruction("cmn x2, #1"); // was the length the -1 until-end sentinel? + emitter.instruction("csel x2, x3, x2, eq"); // use the remaining length when until-end was requested + emitter.instruction("cmp x2, x3"); // does the requested length exceed the remaining tail? + emitter.instruction("csel x2, x3, x2, gt"); // clamp the slice length to the remaining tail + emitter.instruction("str x1, [sp, #16]"); // save the normalized offset + emitter.instruction("str x2, [sp, #24]"); // save the clamped slice length + + // -- create the result hash with capacity = slice_len * 2 (min 16) -- + emitter.instruction("lsl x0, x2, #1"); // x0 = slice length * 2 for hash headroom + emitter.instruction("mov x9, #16"); // x9 = minimum hash capacity + emitter.instruction("cmp x0, x9"); // compare the derived capacity with the minimum + emitter.instruction("csel x0, x9, x0, lt"); // clamp the capacity up to the minimum bucket count + emitter.instruction("ldr x1, [sp, #8]"); // x1 = result value_type tag (source element type) + emitter.instruction("bl __rt_hash_new"); // allocate the result hash table + emitter.instruction("str x0, [sp, #32]"); // save the result hash pointer + emitter.instruction("str xzr, [sp, #40]"); // initialize the loop index to zero + + emitter.label("__rt_aslp_loop"); + emitter.instruction("ldr x4, [sp, #40]"); // reload the loop index + emitter.instruction("ldr x3, [sp, #24]"); // reload the clamped slice length + emitter.instruction("cmp x4, x3"); // have all slice elements been copied? + emitter.instruction("b.ge __rt_aslp_done"); // stop once the whole slice has been inserted + + // -- key = offset + i (preserved original index); value = source[offset + i] -- + emitter.instruction("ldr x5, [sp, #0]"); // reload the source array pointer + emitter.instruction("add x5, x5, #24"); // advance to the source data region + emitter.instruction("ldr x6, [sp, #16]"); // reload the normalized offset + emitter.instruction("add x7, x6, x4"); // x7 = offset + i = preserved integer key + emitter.instruction("ldr x8, [x5, x7, lsl #3]"); // x8 = source[offset + i] scalar value + + // -- insert the preserved-key/value pair (key_hi = -1 marks an integer key) -- + emitter.instruction("ldr x0, [sp, #32]"); // x0 = result hash pointer + emitter.instruction("mov x1, x7"); // x1 = key_lo = preserved original index + emitter.instruction("mov x2, #-1"); // x2 = key_hi sentinel marks an integer key + emitter.instruction("mov x3, x8"); // x3 = value_lo = source scalar value + emitter.instruction("mov x4, #0"); // x4 = value_hi = 0 for scalar payloads + emitter.instruction("ldr x5, [sp, #8]"); // x5 = value_tag = source element type + emitter.instruction("bl __rt_hash_set"); // insert the preserved key/value pair + emitter.instruction("str x0, [sp, #32]"); // update the hash pointer after possible growth + + // -- advance the loop -- + emitter.instruction("ldr x4, [sp, #40]"); // reload the loop index + emitter.instruction("add x4, x4, #1"); // increment the loop index + emitter.instruction("str x4, [sp, #40]"); // save the updated loop index + emitter.instruction("b __rt_aslp_loop"); // continue copying the slice + + emitter.label("__rt_aslp_done"); + emitter.instruction("ldr x0, [sp, #32]"); // x0 = result hash pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the stack frame + emitter.instruction("ret"); // return the result hash + + // -- empty result: offset started beyond the array bounds -- + emitter.label("__rt_aslp_empty"); + emitter.instruction("mov x0, #16"); // minimum hash capacity for the empty result + emitter.instruction("ldr x1, [sp, #8]"); // x1 = result value_type tag + emitter.instruction("bl __rt_hash_new"); // allocate an empty result hash + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the stack frame + emitter.instruction("ret"); // return the empty result hash +} + +/// Emits `__rt_array_slice_preserve` for the x86_64-linux ABI target. +/// +/// Input: rdi = source array, rsi = offset, rdx = length (-1 = until end), rcx = value_type tag +/// Output: rax = new hash table keyed by the preserved original integer indices +fn emit_array_slice_preserve_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_slice_preserve ---"); + emitter.label_global("__rt_array_slice_preserve"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the slice-preserve bookkeeping + emitter.instruction("sub rsp, 48"); // reserve aligned spill slots while keeping nested calls 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source array pointer across nested helper calls + emitter.instruction("mov QWORD PTR [rbp - 16], rcx"); // save the source element value_type tag across nested helper calls + emitter.instruction("mov r9, QWORD PTR [rdi]"); // r9 = source array length + + // -- normalize a negative offset to a positive index (clamped to 0) -- + emitter.instruction("cmp rsi, 0"); // is the requested offset negative? + emitter.instruction("jge __rt_aslp_off_ready_x86"); // skip adjustment when the offset is already non-negative + emitter.instruction("add rsi, r9"); // offset = length + offset for negative offsets + emitter.instruction("cmp rsi, 0"); // is the adjusted offset still before the start? + emitter.instruction("jge __rt_aslp_off_ready_x86"); // keep a now-non-negative adjusted offset + emitter.instruction("xor esi, esi"); // clamp a still-negative offset to zero + emitter.label("__rt_aslp_off_ready_x86"); + emitter.instruction("cmp rsi, r9"); // does the offset start at or beyond the array length? + emitter.instruction("jge __rt_aslp_empty_x86"); // an out-of-range offset yields an empty result hash + + // -- clamp the slice length to the available tail -- + emitter.instruction("mov r8, r9"); // r8 = length + emitter.instruction("sub r8, rsi"); // r8 = remaining = length - offset + emitter.instruction("cmp rdx, -1"); // was the length the -1 until-end sentinel? + emitter.instruction("jne __rt_aslp_have_len_x86"); // keep an explicit requested length + emitter.instruction("mov rdx, r8"); // use the remaining length when until-end was requested + emitter.label("__rt_aslp_have_len_x86"); + emitter.instruction("cmp rdx, r8"); // does the requested length exceed the remaining tail? + emitter.instruction("jle __rt_aslp_len_ok_x86"); // keep a length that already fits the remaining tail + emitter.instruction("mov rdx, r8"); // clamp the slice length to the remaining tail + emitter.label("__rt_aslp_len_ok_x86"); + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the normalized offset + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the clamped slice length + + // -- create the result hash with capacity = slice_len * 2 (min 16) -- + emitter.instruction("mov rdi, rdx"); // rdi = slice length + emitter.instruction("shl rdi, 1"); // rdi = slice length * 2 for hash headroom + emitter.instruction("cmp rdi, 16"); // compare the derived capacity with the minimum + emitter.instruction("jge __rt_aslp_cap_x86"); // keep a capacity that already meets the minimum + emitter.instruction("mov rdi, 16"); // clamp the capacity up to the minimum bucket count + emitter.label("__rt_aslp_cap_x86"); + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // rsi = result value_type tag (source element type) + emitter.instruction("call __rt_hash_new"); // allocate the result hash table + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the result hash pointer + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // initialize the loop index to zero + + emitter.label("__rt_aslp_loop_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the loop index + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // have all slice elements been copied? + emitter.instruction("jge __rt_aslp_done_x86"); // stop once the whole slice has been inserted + + // -- key = offset + i (preserved original index); value = source[offset + i] -- + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source array pointer + emitter.instruction("lea r10, [r10 + 24]"); // advance to the source data region + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the normalized offset + emitter.instruction("add r11, rcx"); // r11 = offset + i = preserved integer key + emitter.instruction("mov rax, QWORD PTR [r10 + r11 * 8]"); // rax = source[offset + i] scalar value + + // -- insert the preserved-key/value pair (key_hi = -1 marks an integer key) -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // rdi = result hash pointer + emitter.instruction("mov rsi, r11"); // rsi = key_lo = preserved original index + emitter.instruction("mov rdx, -1"); // rdx = key_hi sentinel marks an integer key + emitter.instruction("mov rcx, rax"); // rcx = value_lo = source scalar value + emitter.instruction("xor r8d, r8d"); // r8 = value_hi = 0 for scalar payloads + emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // r9 = value_tag = source element type + emitter.instruction("call __rt_hash_set"); // insert the preserved key/value pair + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // update the hash pointer after possible growth + + // -- advance the loop -- + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the loop index + emitter.instruction("add r10, 1"); // increment the loop index + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save the updated loop index + emitter.instruction("jmp __rt_aslp_loop_x86"); // continue copying the slice + + emitter.label("__rt_aslp_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // rax = result hash pointer + emitter.instruction("add rsp, 48"); // deallocate the spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the result hash + + // -- empty result: offset started beyond the array bounds -- + emitter.label("__rt_aslp_empty_x86"); + emitter.instruction("mov rdi, 16"); // minimum hash capacity for the empty result + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // rsi = result value_type tag + emitter.instruction("call __rt_hash_new"); // allocate an empty result hash + emitter.instruction("add rsp, 48"); // deallocate the spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the empty result hash +} diff --git a/src/codegen/runtime/arrays/array_sum_float.rs b/src/codegen/runtime/arrays/array_sum_float.rs new file mode 100644 index 0000000000..6313d39d84 --- /dev/null +++ b/src/codegen/runtime/arrays/array_sum_float.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Emits the `__rt_array_sum_float` runtime helper assembly for floating-point array sum. +//! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Used when `array_sum()` is applied to a `float[]`. Slots hold raw IEEE doubles, so the sum must +//! accumulate with floating-point adds (`fadd`/`addsd`) and return in the float result register +//! (`d0`/`xmm0`), unlike the integer `__rt_array_sum` which adds raw 64-bit words. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_array_sum_float` runtime helper for summing all double elements of an indexed array. +/// +/// Dispatches to the x86_64 Linux variant if the target is `Arch::X86_64`; otherwise emits an ARM64 +/// implementation. Both assume the array pointer is in `x0`/`rdi` and return the sum in the float +/// result register `d0`/`xmm0`. The helper skips the 24-byte array header and iterates over 8-byte +/// double payload slots, accumulating a floating-point sum (0.0 for an empty array). +/// +/// # Arguments +/// * `emitter` - the assembly emitter to write instructions into +pub fn emit_array_sum_float(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_sum_float_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_sum_float ---"); + emitter.label_global("__rt_array_sum_float"); + + // -- set up loop variables -- + emitter.instruction("ldr x9, [x0]"); // x9 = array length from header + emitter.instruction("add x10, x0, #24"); // x10 = base of data region (skip 24-byte header) + emitter.instruction("mov x11, #0"); // x11 = i = 0 (loop counter) + emitter.instruction("movi d0, #0"); // d0 = accumulator = 0.0 (float result register) + + // -- iterate and accumulate floating-point sum -- + emitter.label("__rt_array_sum_float_loop"); + emitter.instruction("cmp x11, x9"); // compare i with array length + emitter.instruction("b.ge __rt_array_sum_float_done"); // if i >= length, we're done + emitter.instruction("ldr d1, [x10, x11, lsl #3]"); // d1 = data[i] as a double + emitter.instruction("fadd d0, d0, d1"); // accumulator += data[i] (floating-point add) + emitter.instruction("add x11, x11, #1"); // i += 1 + emitter.instruction("b __rt_array_sum_float_loop"); // continue loop + + // -- return the sum (already in d0) -- + emitter.label("__rt_array_sum_float_done"); + emitter.instruction("ret"); // return to caller with sum in d0 +} + +/// Emits the x86_64 Linux implementation of `__rt_array_sum_float` using the System V AMD64 ABI. +/// +/// Uses `rdi` for the array pointer, `xmm0` for the accumulator/return value, `rcx` as the loop +/// cursor, `r10` for the array length, and `r11` for the data region base. Skips the 24-byte +/// array header and sums 8-byte double payloads at `rdi + 24 + rcx * 8` with `addsd`. +/// +/// # Arguments +/// * `emitter` - the assembly emitter to write x86_64 instructions into +fn emit_array_sum_float_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_sum_float ---"); + emitter.label_global("__rt_array_sum_float"); + + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before starting the double sum loop + emitter.instruction("lea r11, [rdi + 24]"); // compute the first double payload slot address in the source indexed array + emitter.instruction("xor ecx, ecx"); // initialize the double sum loop cursor at the front of the source indexed array + emitter.instruction("xorps xmm0, xmm0"); // seed the double sum accumulator with 0.0 before visiting any source payloads + + emitter.label("__rt_array_sum_float_loop_x86"); + emitter.instruction("cmp rcx, r10"); // compare the double sum loop cursor against the source indexed-array logical length + emitter.instruction("jge __rt_array_sum_float_done_x86"); // finish once every double payload has contributed to the sum accumulator + emitter.instruction("addsd xmm0, QWORD PTR [r11 + rcx * 8]"); // add the current double payload from the source indexed array into the running sum accumulator + emitter.instruction("add rcx, 1"); // advance the double sum loop cursor after consuming one source payload + emitter.instruction("jmp __rt_array_sum_float_loop_x86"); // continue summing source double payloads until the source array is exhausted + + emitter.label("__rt_array_sum_float_done_x86"); + emitter.instruction("ret"); // return the double sum accumulator in xmm0 +} diff --git a/src/codegen/runtime/arrays/mod.rs b/src/codegen/runtime/arrays/mod.rs index 7a64909015..455e57b242 100644 --- a/src/codegen/runtime/arrays/mod.rs +++ b/src/codegen/runtime/arrays/mod.rs @@ -42,6 +42,8 @@ mod array_intersect_key; mod array_is_list; mod array_key_exists; mod array_map; +mod array_map2; +mod array_map2_str; mod array_map_mixed; mod array_map_str; mod array_merge; @@ -49,11 +51,13 @@ mod array_merge_into; mod array_merge_into_refcounted; mod array_merge_recursive; mod array_merge_refcounted; +mod array_merge_str; mod array_multisort; mod array_new; mod array_pad; mod array_pad_refcounted; mod array_product; +mod array_product_float; mod array_push_int; mod array_push_refcounted; mod array_push_str; @@ -73,10 +77,12 @@ mod array_reverse_refcounted; mod array_search; mod array_shift; mod array_slice; +mod array_slice_preserve; mod array_slice_refcounted; mod array_splice; mod array_splice_refcounted; mod array_sum; +mod array_sum_float; mod array_to_hash; mod array_to_mixed; mod array_udiff_uintersect; @@ -147,6 +153,7 @@ mod mixed_unbox; mod mixed_write_stdout; mod refcount; mod shuffle; +mod sort_float; mod sort_int; mod sort_str; mod undefined_array_key_warning; @@ -219,6 +226,10 @@ pub use array_is_list::emit_array_is_list; pub use array_key_exists::emit_array_key_exists; /// Emit array map helper. pub use array_map::emit_array_map; +/// Emit the two-input-array array map helper. +pub use array_map2::emit_array_map2; +/// Emit the two-input-array string array map helper. +pub use array_map2_str::emit_array_map2_str; /// Emit mixed-result array map helper. pub use array_map_mixed::emit_array_map_mixed; /// Emit string-returning array map helpers. @@ -230,6 +241,8 @@ pub use array_merge_into::emit_array_merge_into; pub use array_merge_into_refcounted::emit_array_merge_into_refcounted; /// Emit refcounted merge-into helper. pub use array_merge_refcounted::emit_array_merge_refcounted; +/// Emit string array merge helper (16-byte ptr+len element slots). +pub use array_merge_str::emit_array_merge_str; /// Emit refcounted array merge helper. pub use array_merge_recursive::{emit_amr_box_value, emit_array_merge_recursive}; /// Emit recursive array merge helpers (array_merge_recursive + scalar-to-list box helper). @@ -242,6 +255,8 @@ pub use array_pad::emit_array_pad; pub use array_pad_refcounted::emit_array_pad_refcounted; /// Emit refcounted array pad helper. pub use array_product::emit_array_product; +/// Emit float array product helper. +pub use array_product_float::emit_array_product_float; /// Emit array product helper. pub use array_push_int::emit_array_push_int; /// Emit integer-optimized array push helper. @@ -280,6 +295,8 @@ pub use array_search::emit_array_search; pub use array_shift::emit_array_shift; /// Emit array shift (remove first) helper. pub use array_slice::emit_array_slice; +/// Emit key-preserving array slice helper (`array_slice($a, $off, $len, true)`). +pub use array_slice_preserve::emit_array_slice_preserve; /// Emit array slice extraction helper. pub use array_slice_refcounted::emit_array_slice_refcounted; /// Emit refcounted array slice helper. @@ -288,6 +305,8 @@ pub use array_splice::emit_array_splice; pub use array_splice_refcounted::emit_array_splice_refcounted; /// Emit refcounted array splice helper. pub use array_sum::emit_array_sum; +/// Emit float array sum helper. +pub use array_sum_float::emit_array_sum_float; /// Emit array sum helper. pub use array_to_hash::emit_array_to_hash; /// Emit indexed-array-to-hash converter helper (shared by hash-based set ops). @@ -418,6 +437,8 @@ pub use refcount::emit_refcount; /// Emit reference count helper. pub use shuffle::emit_shuffle; /// Emit array shuffle helper. +/// Emit float sort helper. +pub use sort_float::emit_sort_float; pub use sort_int::emit_sort_int; /// Emit string sort helper. pub use sort_str::emit_sort_str; diff --git a/src/codegen/runtime/arrays/sort_float.rs b/src/codegen/runtime/arrays/sort_float.rs new file mode 100644 index 0000000000..65da30e25f --- /dev/null +++ b/src/codegen/runtime/arrays/sort_float.rs @@ -0,0 +1,142 @@ +//! Purpose: +//! Emits the `__rt_rsort_float`, `__rt_sort_float` runtime helper assembly for sort float. +//! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Sort helpers mutate array payload order in place; float slots are compared as IEEE doubles +//! (`fcmp`/`ucomisd`) rather than as raw 64-bit integers, so negative and fractional values order +//! correctly. NaN ordering is unspecified (as in PHP) but never loops or traps. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the `__rt_sort_float` (ascending) or `__rt_rsort_float` (descending) runtime helper. +/// Uses ARM64 instructions on non-x86_64 targets; delegates to `emit_sort_float_linux_x86_64` for x86_64. +/// +/// # Arguments +/// * `emitter` — the assembly emitter with target and context +/// * `reverse` — `false` for ascending sort, `true` for descending sort +/// +/// # ABI +/// * Input: array pointer in `x0` +/// * Output: sorted array in-place (no return value register) +/// * Temporaries: integer `x1`–`x7`, float `d4`/`d6` +/// +/// # Algorithm +/// Insertion sort over 8-byte double slots: for each element, shift larger/smaller elements right +/// until the correct insertion position is found. Comparisons use floating-point ordering so the +/// IEEE sign bit and fractional bits do not corrupt the order the way an integer compare would. +pub fn emit_sort_float(emitter: &mut Emitter, reverse: bool) { + if emitter.target.arch == Arch::X86_64 { + emit_sort_float_linux_x86_64(emitter, reverse); + return; + } + + let label = if reverse { "__rt_rsort_float" } else { "__rt_sort_float" }; + let cmp_branch = if reverse { "b.ge" } else { "b.le" }; + + emitter.blank(); + emitter.comment(&format!("--- runtime: {} ---", label)); + emitter.label_global(label); + + // -- load array metadata and set up outer loop -- + emitter.instruction("ldr x1, [x0]"); // x1 = array length from header + emitter.instruction("add x2, x0, #24"); // x2 = base of data region (skip header) + emitter.instruction("mov x3, #1"); // x3 = i = 1 (outer loop index, start at second element) + + let outer = format!("{}_outer", label); + let inner = format!("{}_inner", label); + let insert = format!("{}_insert", label); + let done = format!("{}_done", label); + + // -- outer loop: iterate i from 1 to length-1 -- + emitter.label(&outer); + emitter.instruction("cmp x3, x1"); // compare i with array length + emitter.instruction(&format!("b.ge {}", done)); // if i >= length, sorting is complete + emitter.instruction("ldr d4, [x2, x3, lsl #3]"); // d4 = key = data[i] (double element to insert) + emitter.instruction("sub x5, x3, #1"); // x5 = j = i - 1 (scan backwards from here) + + // -- inner loop: shift elements right until correct position found -- + emitter.label(&inner); + emitter.instruction("cmp x5, #0"); // check if j < 0 + emitter.instruction(&format!("b.lt {}", insert)); // if j < 0, insert position found + emitter.instruction("ldr d6, [x2, x5, lsl #3]"); // d6 = data[j] (double element to compare with key) + emitter.instruction("fcmp d6, d4"); // compare data[j] with key as IEEE doubles + emitter.instruction(&format!("{} {}", cmp_branch, insert)); // if in order (<= or >= for reverse), insert here + emitter.instruction("add x7, x5, #1"); // x7 = j + 1 (destination for shift) + emitter.instruction("str d6, [x2, x7, lsl #3]"); // data[j+1] = data[j] (shift element right) + emitter.instruction("sub x5, x5, #1"); // j -= 1 (move left) + emitter.instruction(&format!("b {}", inner)); // continue inner loop + + // -- insert the key at the correct position -- + emitter.label(&insert); + emitter.instruction("add x7, x5, #1"); // x7 = j + 1 (insertion index) + emitter.instruction("str d4, [x2, x7, lsl #3]"); // data[j+1] = key (place element) + emitter.instruction("add x3, x3, #1"); // i += 1 (advance outer loop) + emitter.instruction(&format!("b {}", outer)); // continue outer loop + + // -- sorting complete -- + emitter.label(&done); + emitter.instruction("ret"); // return to caller +} + +/// x86_64 Linux implementation of the sort-float runtime helpers. +/// Uses System V AMD64 ABI: array pointer in `rdi`, indexed-array length in `[rdi]`. +/// Mirrors the ARM64 behavior but compares double slots with `ucomisd` and the unsigned +/// `jbe`/`jae` branches that match its float-ordering flags. +fn emit_sort_float_linux_x86_64(emitter: &mut Emitter, reverse: bool) { + let label = if reverse { "__rt_rsort_float" } else { "__rt_sort_float" }; + let break_jump = if reverse { + "__rt_sort_float_break_desc" + } else { + "__rt_sort_float_break_asc" + }; + + emitter.blank(); + emitter.comment(&format!("--- runtime: {} ---", label)); + emitter.label_global(label); + + emitter.instruction("mov r8, QWORD PTR [rdi]"); // load the indexed-array logical length before starting the insertion-sort outer loop + emitter.instruction("lea r9, [rdi + 24]"); // compute the indexed-array payload base pointer once so the loop body can index slots directly + emitter.instruction("mov r10, 1"); // initialize the insertion-sort outer-loop cursor to the second element + + let outer = format!("{}_outer", label); + let inner = format!("{}_inner", label); + let insert = format!("{}_insert", label); + let done = format!("{}_done", label); + + emitter.label(&outer); + emitter.instruction("cmp r10, r8"); // compare the insertion-sort outer-loop cursor against the indexed-array logical length + emitter.instruction(&format!("jae {}", done)); // stop once every indexed-array element has been inserted into the sorted prefix + emitter.instruction("movsd xmm0, QWORD PTR [r9 + r10 * 8]"); // load the current indexed-array element as the insertion-sort key (double) + emitter.instruction("mov rcx, r10"); // copy the outer-loop cursor before stepping left through the sorted prefix + emitter.instruction("sub rcx, 1"); // initialize the insertion-sort inner-loop cursor to the element immediately left of the key + + emitter.label(&inner); + emitter.instruction("cmp rcx, -1"); // has the insertion-sort inner-loop cursor stepped past the start of the indexed array? + emitter.instruction(&format!("je {}", insert)); // insert the key at slot zero once the inner loop runs off the left edge + emitter.instruction("movsd xmm1, QWORD PTR [r9 + rcx * 8]"); // load the current sorted-prefix element that is being compared against the key + emitter.instruction("ucomisd xmm1, xmm0"); // compare the current sorted-prefix element against the key using float ordering + if reverse { + emitter.instruction(&format!("jae {}", break_jump)); // stop shifting once the descending-order invariant is satisfied for the current slot + } else { + emitter.instruction(&format!("jbe {}", break_jump)); // stop shifting once the ascending-order invariant is satisfied for the current slot + } + emitter.instruction("movsd QWORD PTR [r9 + rcx * 8 + 8], xmm1"); // shift the current sorted-prefix element one slot to the right to make room for the key + emitter.instruction("sub rcx, 1"); // move the insertion-sort inner-loop cursor one slot further left through the sorted prefix + emitter.instruction(&format!("jmp {}", inner)); // continue scanning left until the correct insertion point is found + + emitter.label(break_jump); + emitter.instruction(&format!("jmp {}", insert)); // jump into the shared insertion path once the sorted-prefix comparison says the key belongs here + + emitter.label(&insert); + emitter.instruction("movsd QWORD PTR [r9 + rcx * 8 + 8], xmm0"); // store the insertion-sort key into the slot immediately to the right of the final inner-loop cursor + emitter.instruction("add r10, 1"); // advance the insertion-sort outer-loop cursor to the next indexed-array element + emitter.instruction(&format!("jmp {}", outer)); // continue insertion-sorting the remaining indexed-array suffix + + emitter.label(&done); + emitter.instruction("ret"); // return after sorting the indexed-array payload in place +} diff --git a/src/codegen/runtime/emitters.rs b/src/codegen/runtime/emitters.rs index 246bee71b2..dc3f7ab25c 100644 --- a/src/codegen/runtime/emitters.rs +++ b/src/codegen/runtime/emitters.rs @@ -199,6 +199,8 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_random_uniform(emitter); arrays::emit_sort_int(emitter, false); arrays::emit_sort_int(emitter, true); + arrays::emit_sort_float(emitter, false); + arrays::emit_sort_float(emitter, true); arrays::emit_sort_str(emitter, false); arrays::emit_sort_str(emitter, true); arrays::emit_hash_fnv1a(emitter); @@ -226,12 +228,16 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_array_reverse(emitter); arrays::emit_array_reverse_refcounted(emitter); arrays::emit_array_sum(emitter); + arrays::emit_array_sum_float(emitter); arrays::emit_array_product(emitter); + arrays::emit_array_product_float(emitter); arrays::emit_array_shift(emitter); arrays::emit_array_unshift(emitter); arrays::emit_array_merge(emitter); arrays::emit_array_merge_refcounted(emitter); + arrays::emit_array_merge_str(emitter); arrays::emit_array_slice(emitter); + arrays::emit_array_slice_preserve(emitter); arrays::emit_array_slice_refcounted(emitter); arrays::emit_range(emitter); arrays::emit_shuffle(emitter); @@ -277,6 +283,8 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_ksort(emitter); arrays::emit_natsort(emitter); arrays::emit_array_map(emitter); + arrays::emit_array_map2(emitter); + arrays::emit_array_map2_str(emitter); arrays::emit_array_map_mixed(emitter); arrays::emit_array_map_str(emitter); arrays::emit_array_map_str_owned(emitter); diff --git a/src/codegen/runtime/strings/number_format.rs b/src/codegen/runtime/strings/number_format.rs index 8616f08621..bc633b9316 100644 --- a/src/codegen/runtime/strings/number_format.rs +++ b/src/codegen/runtime/strings/number_format.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_number_format`, `__rt_nf_count` runtime helper assembly for number format. +//! Emits the `__rt_number_format` runtime helper assembly for number formatting. //! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,6 +7,8 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - Both separators are full `(ptr, len)` strings: a length of 0 means "insert no separator", +//! and multi-byte separators are copied byte-for-byte (so a non-breaking-space separator works). use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; @@ -16,84 +18,93 @@ use crate::codegen::platform::Arch; /// Formats a floating-point number with configurable decimal places and separators, /// writing the result into the concat buffer. Dispatches to target-specific implementations. /// -/// Input registers (ARM64): `d0` = number, `x1` = decimals, `x2` = dec_point char, `x3` = thousands_sep (0=none) +/// Input registers (ARM64): `d0` = number, `x1` = decimals, `x2`/`x3` = dec-separator ptr/len, `x4`/`x5` = thousands-separator ptr/len /// Output registers (ARM64): `x1` = string pointer, `x2` = string length -/// Input registers (x86_64 SysV): `xmm0` = number, `rdi` = decimals, `rsi` = dec_point, `rdx` = thousands_sep +/// Input registers (x86_64 SysV): `xmm0` = number, `rdi` = decimals, `rsi`/`rdx` = dec-separator ptr/len, `rcx`/`r8` = thousands-separator ptr/len /// Output registers (x86_64 SysV): `rax` = string pointer, `rdx` = string length /// -/// Stack frame layout (ARM64, 128 bytes): -/// `[sp+0..47]` snprintf buffer (48 bytes) -/// `[sp+64..68]` format string `"%.Nf\0"` -/// `[sp+72]` result start ptr -/// `[sp+80]` raw snprintf length -/// `[sp+88]` number (double) -/// `[sp+96]` decimals -/// `[sp+100]` dec_point char -/// `[sp+104]` thousands_sep char -/// `[sp+112]` saved x29, x30 +/// Stack frame layout (ARM64, 160 bytes): +/// `[sp+0..47]` snprintf buffer (48 bytes) +/// `[sp+48..52]` format string `"%.Nf\0"` +/// `[sp+56]` result start ptr +/// `[sp+64]` raw snprintf length +/// `[sp+72]` number (double) +/// `[sp+80]` decimals +/// `[sp+88]` dec-separator ptr +/// `[sp+96]` dec-separator len +/// `[sp+104]` thousands-separator ptr +/// `[sp+112]` thousands-separator len +/// `[sp+144]` saved x29, x30 pub fn emit_number_format(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_number_format_linux_x86_64(emitter); return; } - // Stack frame layout (128 bytes): - // [sp+0..47] snprintf buffer (48 bytes) - // [sp+64..68] format string "%.Nf\0" - // [sp+72] result start ptr - // [sp+80] raw_len - // [sp+88] number (d0) - // [sp+96] decimals - // [sp+100] dec_point char - // [sp+104] thousands_sep char - // [sp+112] saved x29, x30 emitter.blank(); emitter.comment("--- runtime: number_format ---"); emitter.label_global("__rt_number_format"); - // -- set up stack frame (128 bytes) -- - emitter.instruction("sub sp, sp, #128"); // allocate 128 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #112"); // establish new frame pointer + // -- set up stack frame (160 bytes) -- + emitter.instruction("sub sp, sp, #160"); // allocate 160 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #144]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #144"); // establish new frame pointer // -- save input arguments -- - emitter.instruction("str x1, [sp, #96]"); // save decimals count - emitter.instruction("str d0, [sp, #88]"); // save the floating-point number - emitter.instruction("str x2, [sp, #100]"); // save decimal point character - emitter.instruction("str x3, [sp, #104]"); // save thousands separator character + emitter.instruction("str d0, [sp, #72]"); // save the floating-point number + emitter.instruction("str x1, [sp, #80]"); // save the decimals count + emitter.instruction("str x2, [sp, #88]"); // save the decimal-separator pointer + emitter.instruction("str x3, [sp, #96]"); // save the decimal-separator length + emitter.instruction("str x4, [sp, #104]"); // save the thousands-separator pointer + emitter.instruction("str x5, [sp, #112]"); // save the thousands-separator length - // -- build format string "%.Nf" at [sp+64] -- + // -- pre-round the value half-away-from-zero to `decimals` places (PHP number_format() uses + // round-half-up; snprintf %.*f would otherwise round half-to-even, e.g. 2.5 -> "2") -- + emitter.instruction("ldr x9, [sp, #80]"); // reload the decimals count + emitter.instruction("scvtf d1, x9"); // convert decimals into a floating exponent for pow() + emitter.instruction("str d0, [sp, #-16]!"); // preserve the input value across the pow() call + emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the precision multiplier base + emitter.bl_c("pow"); // d0 = 10^decimals (the precision multiplier) + emitter.instruction("ldr d1, [sp], #16"); // restore the input value into d1 + emitter.instruction("fmul d1, d1, d0"); // scale the value by the precision multiplier + emitter.instruction("str d0, [sp, #-16]!"); // preserve the multiplier for the final division + emitter.instruction("frinta d0, d1"); // round the scaled value to nearest, ties away from zero + emitter.instruction("ldr d1, [sp], #16"); // restore the multiplier into d1 + emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to the requested precision + emitter.instruction("str d0, [sp, #72]"); // store the pre-rounded value for snprintf to format + + // -- build format string "%.Nf" at [sp+48] -- emitter.instruction("mov w9, #37"); // ASCII '%' - emitter.instruction("strb w9, [sp, #64]"); // write '%' to format string + emitter.instruction("strb w9, [sp, #48]"); // write '%' to format string emitter.instruction("mov w9, #46"); // ASCII '.' - emitter.instruction("strb w9, [sp, #65]"); // write '.' to format string - emitter.instruction("ldr x9, [sp, #96]"); // load decimals count + emitter.instruction("strb w9, [sp, #49]"); // write '.' to format string + emitter.instruction("ldr x9, [sp, #80]"); // load decimals count emitter.instruction("add w9, w9, #48"); // convert to ASCII digit ('0' + N) - emitter.instruction("strb w9, [sp, #66]"); // write decimal count digit + emitter.instruction("strb w9, [sp, #50]"); // write decimal count digit emitter.instruction("mov w9, #102"); // ASCII 'f' - emitter.instruction("strb w9, [sp, #67]"); // write 'f' format specifier - emitter.instruction("strb wzr, [sp, #68]"); // null-terminate the format string + emitter.instruction("strb w9, [sp, #51]"); // write 'f' format specifier + emitter.instruction("strb wzr, [sp, #52]"); // null-terminate the format string // -- call snprintf(buf, 48, fmt, d0) -- emitter.instruction("add x0, sp, #0"); // x0 = output buffer at start of stack frame emitter.instruction("mov x1, #48"); // buffer size = 48 bytes - emitter.instruction("add x2, sp, #64"); // x2 = format string pointer - emitter.instruction("ldr d0, [sp, #88]"); // reload the float value + emitter.instruction("add x2, sp, #48"); // x2 = format string pointer + emitter.instruction("ldr d0, [sp, #72]"); // reload the float value emitter.instruction("str d0, [sp, #-16]!"); // push double for variadic ABI, adjust sp emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 emitter.instruction("add sp, sp, #16"); // pop the variadic argument from stack - emitter.instruction("str x0, [sp, #80]"); // save raw string length + emitter.instruction("str x0, [sp, #64]"); // save raw string length // -- set up concat_buf destination -- crate::codegen::abi::emit_symbol_address(emitter, "x6", "_concat_off"); emitter.instruction("ldr x8, [x6]"); // load current concat_buf write offset crate::codegen::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); emitter.instruction("add x10, x7, x8"); // compute destination pointer - emitter.instruction("str x10, [sp, #72]"); // save result start pointer + emitter.instruction("str x10, [sp, #56]"); // save result start pointer // -- scan raw string to find integer part length -- emitter.instruction("add x11, sp, #0"); // x11 = source ptr (snprintf output) - emitter.instruction("ldr x12, [sp, #80]"); // x12 = raw string length + emitter.instruction("ldr x12, [sp, #64]"); // x12 = raw string length emitter.instruction("mov x13, #0"); // x13 = integer part digit count // -- handle leading minus sign -- @@ -132,13 +143,21 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("cmp x16, x13"); // check if all integer digits copied emitter.instruction("b.ge __rt_nf_decimal"); // if done, move to decimal part - // -- insert thousands separator between groups -- + // -- insert thousands separator string between groups -- emitter.instruction("cbz x16, __rt_nf_no_sep"); // skip separator before first digit emitter.instruction("cmp x14, #0"); // check if current group is exhausted emitter.instruction("b.ne __rt_nf_no_sep"); // group not done, no separator yet - emitter.instruction("ldr x9, [sp, #104]"); // load thousands separator char - emitter.instruction("cbz x9, __rt_nf_no_sep_reset"); // skip if separator is 0 (none) - emitter.instruction("strb w9, [x10], #1"); // write separator to output, advance dest + emitter.instruction("ldr x9, [sp, #112]"); // load thousands separator length + emitter.instruction("cbz x9, __rt_nf_no_sep_reset"); // skip if separator is empty (length 0) + emitter.instruction("ldr x7, [sp, #104]"); // load thousands separator pointer + emitter.instruction("mov x8, #0"); // separator byte index + emitter.label("__rt_nf_sep_copy"); + emitter.instruction("cmp x8, x9"); // have all separator bytes been written? + emitter.instruction("b.ge __rt_nf_no_sep_reset"); // separator fully copied, resume digits + emitter.instruction("ldrb w11, [x7, x8]"); // load next separator byte + emitter.instruction("strb w11, [x10], #1"); // write separator byte to output, advance dest + emitter.instruction("add x8, x8, #1"); // advance separator byte index + emitter.instruction("b __rt_nf_sep_copy"); // continue copying separator bytes emitter.label("__rt_nf_no_sep_reset"); emitter.instruction("mov x14, #3"); // reset group counter for next 3 digits @@ -149,35 +168,56 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("sub x14, x14, #1"); // decrement group counter emitter.instruction("b __rt_nf_copy_int"); // continue copying integer digits - // -- copy decimal part, replacing '.' with custom separator -- + // -- copy decimal part, replacing '.' with custom separator string -- emitter.label("__rt_nf_decimal"); emitter.instruction("add x15, x15, x13"); // advance source past integer digits emitter.label("__rt_nf_copy_dec"); emitter.instruction("cbz x12, __rt_nf_done"); // if no decimal chars remain, done emitter.instruction("ldrb w9, [x15], #1"); // load next decimal char, advance source + emitter.instruction("sub x12, x12, #1"); // consume one raw decimal char emitter.instruction("cmp w9, #46"); // check if it's '.' (snprintf decimal point) - emitter.instruction("b.ne __rt_nf_dec_store"); // if not '.', store as-is - emitter.instruction("ldr x9, [sp, #100]"); // replace with custom decimal point char + emitter.instruction("b.ne __rt_nf_dec_store"); // if not '.', store the byte as-is + emitter.instruction("ldr x9, [sp, #96]"); // load custom decimal-separator length + emitter.instruction("cbz x9, __rt_nf_copy_dec"); // empty separator → write nothing, continue + emitter.instruction("ldr x7, [sp, #88]"); // load custom decimal-separator pointer + emitter.instruction("mov x8, #0"); // separator byte index + emitter.label("__rt_nf_dec_sep_copy"); + emitter.instruction("cmp x8, x9"); // have all decimal-separator bytes been written? + emitter.instruction("b.ge __rt_nf_copy_dec"); // separator fully copied, resume decimal part + emitter.instruction("ldrb w11, [x7, x8]"); // load next decimal-separator byte + emitter.instruction("strb w11, [x10], #1"); // write separator byte to output, advance dest + emitter.instruction("add x8, x8, #1"); // advance separator byte index + emitter.instruction("b __rt_nf_dec_sep_copy"); // continue copying decimal-separator bytes emitter.label("__rt_nf_dec_store"); - emitter.instruction("strb w9, [x10], #1"); // write char to output, advance dest - emitter.instruction("sub x12, x12, #1"); // decrement remaining chars + emitter.instruction("strb w9, [x10], #1"); // write decimal digit to output, advance dest emitter.instruction("b __rt_nf_copy_dec"); // continue copying decimal part // -- finalize: compute length and update concat_off -- emitter.label("__rt_nf_done"); - emitter.instruction("ldr x1, [sp, #72]"); // load result start pointer + emitter.instruction("ldr x1, [sp, #56]"); // load result start pointer emitter.instruction("sub x2, x10, x1"); // result length = dest_end - dest_start emitter.instruction("ldr x8, [x6]"); // load current concat_off emitter.instruction("add x8, x8, x2"); // advance offset by result length emitter.instruction("str x8, [x6]"); // store updated concat_off // -- restore frame and return -- - emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #128"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #144]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #160"); // deallocate stack frame emitter.instruction("ret"); // return to caller } -/// ARM64 implementation of the `__rt_number_format` runtime helper. +/// x86_64 implementation of the `__rt_number_format` runtime helper. +/// +/// Mirrors the AArch64 implementation: renders the raw fixed-point string with +/// snprintf, then re-emits it into the concat buffer inserting the thousands +/// separator string between 3-digit groups and translating the snprintf decimal +/// point into the configured decimal-separator string. Both separators are full +/// `(ptr, len)` strings; a length of 0 inserts nothing. +/// +/// Stack frame layout (x86_64): `[rbp-120..-73]` snprintf buffer, `[rbp-72..-68]` +/// mini format string, `[rbp-64]` raw length, `[rbp-56]` decimals, `[rbp-48]` +/// dec-separator ptr, `[rbp-40]` dec-separator len, `[rbp-128]` thousands ptr, +/// `[rbp-136]` thousands len. fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: number_format ---"); @@ -188,10 +228,27 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbx"); // preserve the concat-buffer destination cursor across the local formatting and copy loops emitter.instruction("push r12"); // preserve the concat-buffer start pointer for the final x86_64 string return pair emitter.instruction("push r13"); // preserve the concat-offset symbol address across the local formatting and copy loops - emitter.instruction("sub rsp, 104"); // reserve local storage; bumped 96→104 so the four 8-byte saves above + this sub leave rsp 0-mod-16 before the SysV snprintf call below + emitter.instruction("sub rsp, 120"); // reserve local storage; 120 keeps the four 8-byte saves above + this sub 0-mod-16 before the SysV snprintf call below emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // preserve the requested decimal count across the intermediate formatting and copy loops - emitter.instruction("mov QWORD PTR [rbp - 48], rsi"); // preserve the decimal-separator byte across the intermediate formatting and copy loops - emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // preserve the thousands-separator byte across the intermediate formatting and copy loops + emitter.instruction("mov QWORD PTR [rbp - 48], rsi"); // preserve the decimal-separator pointer across the intermediate formatting and copy loops + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // preserve the decimal-separator length across the intermediate formatting and copy loops + emitter.instruction("mov QWORD PTR [rbp - 128], rcx"); // preserve the thousands-separator pointer across the intermediate formatting and copy loops + emitter.instruction("mov QWORD PTR [rbp - 136], r8"); // preserve the thousands-separator length across the intermediate formatting and copy loops + // -- pre-round the value (still in xmm0) half-away-from-zero to `decimals` places (PHP + // number_format() uses round-half-up; snprintf %.*f would otherwise round half-to-even) -- + crate::codegen::abi::emit_push_float_reg(emitter, "xmm0"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the decimals count as the pow() exponent + emitter.instruction("cvtsi2sd xmm1, rax"); // convert decimals into a floating exponent for pow() + emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 payload for 10.0 + emitter.instruction("movq xmm0, rax"); // move 10.0 into the first pow() argument + emitter.bl_c("pow"); // xmm0 = 10^decimals (the precision multiplier) + crate::codegen::abi::emit_pop_float_reg(emitter, "xmm1"); // restore the input value into xmm1 + emitter.instruction("mulsd xmm1, xmm0"); // scale the value by the precision multiplier + crate::codegen::abi::emit_push_float_reg(emitter, "xmm0"); // preserve the multiplier for the final division + emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round() argument register + emitter.bl_c("round"); // round the scaled value to nearest, ties away from zero + crate::codegen::abi::emit_pop_float_reg(emitter, "xmm1"); // restore the multiplier into xmm1 + emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to the requested precision emitter.instruction("mov BYTE PTR [rbp - 72], 37"); // seed the mini format string with the leading '%' introducer emitter.instruction("mov BYTE PTR [rbp - 71], 46"); // append the '.' precision introducer to the mini format string emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the requested decimal count before converting it into the single supported ASCII precision digit @@ -199,12 +256,12 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov BYTE PTR [rbp - 70], r8b"); // append the ASCII precision digit to the mini format string emitter.instruction("mov BYTE PTR [rbp - 69], 102"); // append the trailing 'f' format type so snprintf renders a fixed-point decimal string emitter.instruction("mov BYTE PTR [rbp - 68], 0"); // null-terminate the mini format string before handing it to snprintf - emitter.instruction("lea rdi, [rbp - 120]"); // point snprintf at the fixed local raw-decimal buffer that will be post-processed for thousands separators + emitter.instruction("lea rdi, [rbp - 120]"); // point snprintf at the fixed local raw-decimal buffer that will be post-processed for separators emitter.instruction("mov esi, 48"); // bound the raw-decimal buffer to 48 bytes before the variadic snprintf call emitter.instruction("lea rdx, [rbp - 72]"); // pass the mini format string to snprintf as the fixed-point format pointer emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register because the formatted number is passed in xmm0 on SysV x86_64 emitter.bl_c("snprintf"); // render the raw fixed-point decimal string into the local snprintf buffer - emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // preserve the raw snprintf byte count before the thousands-separator pass consumes caller-saved registers + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // preserve the raw snprintf byte count before the separator pass consumes caller-saved registers crate::codegen::abi::emit_symbol_address(emitter, "r13", "_concat_off"); emitter.instruction("mov r8, QWORD PTR [r13]"); // load the current concat-buffer write cursor before appending the formatted output crate::codegen::abi::emit_symbol_address(emitter, "r9", "_concat_buf"); @@ -251,11 +308,19 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_nf_no_sep_linux_x86_64"); // skip separator insertion before the first emitted integer digit emitter.instruction("test r8, r8"); // has the current thousands group been exhausted exactly at this copy position? emitter.instruction("jnz __rt_nf_no_sep_linux_x86_64"); // skip separator insertion until the current thousands group has been exhausted - emitter.instruction("mov r9, QWORD PTR [rbp - 40]"); // reload the configured thousands-separator byte before deciding whether to emit it - emitter.instruction("test r9, r9"); // is thousands grouping disabled because the configured separator byte is zero? + emitter.instruction("mov r9, QWORD PTR [rbp - 136]"); // reload the configured thousands-separator length before deciding whether to emit it + emitter.instruction("test r9, r9"); // is thousands grouping disabled because the configured separator is empty? emitter.instruction("jz __rt_nf_no_sep_reset_linux_x86_64"); // skip emitting a separator when the caller requested no thousands separator - emitter.instruction("mov BYTE PTR [rbx], r9b"); // append the configured thousands-separator byte to the concat buffer - emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after inserting one thousands separator byte + emitter.instruction("mov r10, QWORD PTR [rbp - 128]"); // reload the configured thousands-separator pointer before copying its bytes + emitter.instruction("xor eax, eax"); // start the thousands-separator byte index at zero + emitter.label("__rt_nf_sep_copy_linux_x86_64"); + emitter.instruction("cmp rax, r9"); // have all thousands-separator bytes been written? + emitter.instruction("jge __rt_nf_no_sep_reset_linux_x86_64"); // resume copying digits once the separator string is fully emitted + emitter.instruction("movzx edx, BYTE PTR [r10 + rax]"); // load the next thousands-separator byte + emitter.instruction("mov BYTE PTR [rbx], dl"); // append the thousands-separator byte to the concat buffer + emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after one separator byte + emitter.instruction("add rax, 1"); // advance the thousands-separator byte index + emitter.instruction("jmp __rt_nf_sep_copy_linux_x86_64"); // continue copying the thousands-separator bytes emitter.label("__rt_nf_no_sep_reset_linux_x86_64"); emitter.instruction("mov r8, 3"); // reset the remaining width of the next thousands group after crossing a group boundary @@ -274,14 +339,25 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jz __rt_nf_done_linux_x86_64"); // finish once the full decimal part has been copied or omitted emitter.instruction("movzx eax, BYTE PTR [r11]"); // load the next raw decimal-part byte before checking whether it is the snprintf decimal point emitter.instruction("add r11, 1"); // advance the raw formatted cursor after loading one decimal-part byte + emitter.instruction("sub rcx, 1"); // decrement the remaining raw decimal-part byte count after consuming one byte emitter.instruction("cmp al, 46"); // is the current decimal-part byte the '.' decimal-point separator emitted by snprintf? emitter.instruction("jne __rt_nf_store_dec_linux_x86_64"); // copy non-decimal-point bytes directly into the concat buffer without translation - emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload the configured decimal-separator byte before replacing the snprintf decimal point - emitter.instruction("mov eax, r9d"); // replace the raw snprintf decimal-point byte with the configured decimal-separator byte + emitter.instruction("mov r9, QWORD PTR [rbp - 40]"); // reload the configured decimal-separator length before replacing the snprintf decimal point + emitter.instruction("test r9, r9"); // is the configured decimal separator empty? + emitter.instruction("jz __rt_nf_copy_dec_linux_x86_64"); // empty decimal separator → write nothing and continue the decimal part + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the configured decimal-separator pointer before copying its bytes + emitter.instruction("xor eax, eax"); // start the decimal-separator byte index at zero + emitter.label("__rt_nf_dec_sep_copy_linux_x86_64"); + emitter.instruction("cmp rax, r9"); // have all decimal-separator bytes been written? + emitter.instruction("jge __rt_nf_copy_dec_linux_x86_64"); // resume copying the decimal part once the separator string is fully emitted + emitter.instruction("movzx edx, BYTE PTR [r10 + rax]"); // load the next decimal-separator byte + emitter.instruction("mov BYTE PTR [rbx], dl"); // append the decimal-separator byte to the concat buffer + emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after one separator byte + emitter.instruction("add rax, 1"); // advance the decimal-separator byte index + emitter.instruction("jmp __rt_nf_dec_sep_copy_linux_x86_64"); // continue copying the decimal-separator bytes emitter.label("__rt_nf_store_dec_linux_x86_64"); - emitter.instruction("mov BYTE PTR [rbx], al"); // append the current decimal-part byte to the concat buffer after any separator translation + emitter.instruction("mov BYTE PTR [rbx], al"); // append the current decimal-part digit to the concat buffer emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after copying one decimal-part byte - emitter.instruction("sub rcx, 1"); // decrement the remaining raw decimal-part byte count after copying one byte emitter.instruction("jmp __rt_nf_copy_dec_linux_x86_64"); // continue copying the decimal part until every remaining raw byte has been emitted emitter.label("__rt_nf_done_linux_x86_64"); @@ -291,7 +367,7 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r8, QWORD PTR [r13]"); // reload the old concat-buffer write cursor before publishing the formatted-string append emitter.instruction("add r8, rdx"); // advance the concat-buffer write cursor by the emitted formatted-string length emitter.instruction("mov QWORD PTR [r13], r8"); // publish the updated concat-buffer write cursor after appending the formatted number - emitter.instruction("add rsp, 104"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers + emitter.instruction("add rsp, 120"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers emitter.instruction("pop r13"); // restore the saved concat-offset symbol register after the x86_64 number_format() helper finishes emitter.instruction("pop r12"); // restore the saved concat-buffer start register after the x86_64 number_format() helper finishes emitter.instruction("pop rbx"); // restore the saved concat-buffer destination cursor register after the x86_64 number_format() helper finishes diff --git a/src/codegen_ir/lower_inst.rs b/src/codegen_ir/lower_inst.rs index 70139ad03b..7b16516703 100644 --- a/src/codegen_ir/lower_inst.rs +++ b/src/codegen_ir/lower_inst.rs @@ -101,7 +101,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::FAdd => floats::lower_float_binop(ctx, &inst, "fadd", "addsd"), Op::FSub => floats::lower_float_binop(ctx, &inst, "fsub", "subsd"), Op::FMul => floats::lower_float_binop(ctx, &inst, "fmul", "mulsd"), - Op::FDiv => floats::lower_float_binop(ctx, &inst, "fdiv", "divsd"), + Op::FDiv => floats::lower_float_div(ctx, &inst), Op::FPow => floats::lower_float_pow(ctx, &inst), Op::FNeg => floats::lower_float_neg(ctx, &inst), Op::ICmp => lower_int_compare(ctx, &inst), diff --git a/src/codegen_ir/lower_inst/arithmetic.rs b/src/codegen_ir/lower_inst/arithmetic.rs index c88566dc01..40183ec4ea 100644 --- a/src/codegen_ir/lower_inst/arithmetic.rs +++ b/src/codegen_ir/lower_inst/arithmetic.rs @@ -42,7 +42,10 @@ pub(super) fn lower_int_binop( store_if_result(ctx, inst) } -/// Lowers a signed integer modulo operation with the legacy backend's zero-divisor guard. +/// Lowers a signed integer modulo operation, raising an uncatchable fatal when the divisor is zero. +/// +/// PHP raises a catchable `DivisionByZeroError` on `% 0`; elephc has no exception unwinding, so a +/// zero divisor branches to a `Fatal error: modulo by zero` diagnostic and exits. pub(super) fn lower_int_mod(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; @@ -55,26 +58,25 @@ pub(super) fn lower_int_mod(ctx: &mut FunctionContext<'_>, inst: &Instruction) - match ctx.emitter.target.arch { Arch::AArch64 => { let quotient_reg = abi::tertiary_scratch_reg(ctx.emitter); - ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // branch to zero-divisor guard when modulo divisor is zero + ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // fatal when the modulo divisor is zero (PHP DivisionByZeroError) ctx.emitter.instruction(&format!("sdiv {}, {}, {}", quotient_reg, result_reg, rhs_reg)); // compute signed quotient for the modulo operation ctx.emitter.instruction(&format!("msub {}, {}, {}, {}", result_reg, quotient_reg, rhs_reg, result_reg)); // compute left - quotient * right as the remainder - ctx.emitter.instruction(&format!("b {}", done_label)); // skip the modulo zero fallback after a normal remainder - ctx.emitter.label(&zero_label); - ctx.emitter.instruction(&format!("mov {}, #0", result_reg)); // return zero for modulo by zero to match the legacy backend - ctx.emitter.label(&done_label); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the zero-divisor fatal after a normal remainder } Arch::X86_64 => { ctx.emitter.instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the modulo divisor is zero - ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to zero-divisor guard when modulo divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // fatal when the modulo divisor is zero (PHP DivisionByZeroError) ctx.emitter.instruction("cqo"); // sign-extend the dividend before signed division ctx.emitter.instruction(&format!("idiv {}", rhs_reg)); // divide signed integers with quotient in rax and remainder in rdx ctx.emitter.instruction(&format!("mov {}, rdx", result_reg)); // move the signed remainder into the integer result register - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the modulo zero fallback after a normal remainder - ctx.emitter.label(&zero_label); - ctx.emitter.instruction(&format!("mov {}, 0", result_reg)); // return zero for modulo by zero to match the legacy backend - ctx.emitter.label(&done_label); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the zero-divisor fatal after a normal remainder } } + ctx.emitter.label(&zero_label); + // PHP raises a catchable DivisionByZeroError on `% 0`; elephc has no exception + // unwinding, so it emits an uncatchable fatal that mirrors the legacy backend. + abi::emit_fatal_to_stderr(ctx.emitter, ctx.data, b"Fatal error: modulo by zero\n"); + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -89,18 +91,29 @@ pub(super) fn lower_int_div_to_float( let rhs_reg = abi::tertiary_scratch_reg(ctx.emitter); load_integer_operand(ctx, lhs, lhs_reg, inst)?; load_integer_operand(ctx, rhs, rhs_reg, inst)?; + let zero_label = ctx.next_label("div_zero"); + let done_label = ctx.next_label("div_done"); match ctx.emitter.target.arch { Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // fatal when the divisor is zero (PHP DivisionByZeroError) ctx.emitter.instruction(&format!("scvtf d0, {}", lhs_reg)); // promote the integer dividend into the float result register ctx.emitter.instruction(&format!("scvtf d1, {}", rhs_reg)); // promote the integer divisor into a float scratch register ctx.emitter.instruction("fdiv d0, d0, d1"); // divide promoted operands as PHP floating-point division + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the zero-divisor fatal after a normal quotient } Arch::X86_64 => { + ctx.emitter.instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // fatal when the divisor is zero (PHP DivisionByZeroError) ctx.emitter.instruction(&format!("cvtsi2sd xmm0, {}", lhs_reg)); // promote the integer dividend into the float result register ctx.emitter.instruction(&format!("cvtsi2sd xmm1, {}", rhs_reg)); // promote the integer divisor into a float scratch register ctx.emitter.instruction("divsd xmm0, xmm1"); // divide promoted operands as PHP floating-point division + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the zero-divisor fatal after a normal quotient } } + ctx.emitter.label(&zero_label); + // PHP raises a catchable DivisionByZeroError on `/ 0`; elephc fatals instead. + abi::emit_fatal_to_stderr(ctx.emitter, ctx.data, b"Fatal error: division by zero\n"); + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } diff --git a/src/codegen_ir/lower_inst/builtins/arrays.rs b/src/codegen_ir/lower_inst/builtins/arrays.rs index a2710567be..07d0633c72 100644 --- a/src/codegen_ir/lower_inst/builtins/arrays.rs +++ b/src/codegen_ir/lower_inst/builtins/arrays.rs @@ -49,12 +49,18 @@ pub(crate) fn lower_call_user_func_builtin_escape( /// Lowers `array_sum()` over supported indexed-array payloads. pub(crate) fn lower_array_sum(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_aggregate(ctx, inst, "array_sum", "__rt_array_sum") + lower_indexed_array_aggregate(ctx, inst, "array_sum", "__rt_array_sum", "__rt_array_sum_float") } /// Lowers `array_product()` over supported indexed-array payloads. pub(crate) fn lower_array_product(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_aggregate(ctx, inst, "array_product", "__rt_array_product") + lower_indexed_array_aggregate( + ctx, + inst, + "array_product", + "__rt_array_product", + "__rt_array_product_float", + ) } /// Lowers `array_push()` by appending one value and publishing the mutated array. @@ -310,6 +316,9 @@ pub(crate) fn lower_array_filter(ctx: &mut FunctionContext<'_>, inst: &Instructi /// Lowers `array_map()` through the callback runtime helper matching the callback result type. pub(crate) fn lower_array_map(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.len() == 3 { + return lower_array_map2(ctx, inst); + } super::ensure_arg_count(inst, "array_map", 2)?; let callback = expect_operand(inst, 0)?; let array = expect_operand(inst, 1)?; @@ -411,6 +420,103 @@ pub(crate) fn lower_array_map(ctx: &mut FunctionContext<'_>, inst: &Instruction) store_if_result(ctx, inst) } +/// Lowers the two-input-array form `array_map($callback, $a, $b)` (bounded H11 subset). +/// +/// The checker restricts this path to two arrays sharing one scalar element type (both integer or +/// both string) with a callback returning that same type, so the visible callback argument types and +/// the result element type are all that shared element type. Dispatches every supported callback +/// shape (closure descriptor, runtime-string name, closure-holding local, or static binding) through +/// the same `__rt_array_map2`/`__rt_array_map2_str` runtime the shared helpers select on element type. +fn lower_array_map2(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let callback = expect_operand(inst, 0)?; + let array0 = expect_operand(inst, 1)?; + let array1 = expect_operand(inst, 2)?; + let elem_ty = array_map_callback_array_element_type(ctx.value_php_type(array0)?)?; + let runtime_label = if elem_ty == PhpType::Str { + "__rt_array_map2_str" + } else { + "__rt_array_map2" + }; + let visible_arg_types = vec![elem_ty.clone(), elem_ty.clone()]; + let result_elem_ty = elem_ty.clone(); + match ctx.value_php_type(callback)?.codegen_repr() { + PhpType::Callable => { + lower_descriptor_callback_runtime( + ctx, + callback, + visible_arg_types.clone(), + result_elem_ty.clone(), + |ctx, wrapper_label, env_bytes| { + emit_array_map2_call(ctx, wrapper_label, array0, array1, env_bytes, runtime_label) + }, + )?; + } + PhpType::Str => { + lower_runtime_string_descriptor_callback( + ctx, + callback, + Some(&PhpType::Array(Box::new(elem_ty.clone()))), + visible_arg_types.clone(), + result_elem_ty.clone(), + "array_map", + |ctx, wrapper_label, env_bytes| { + emit_array_map2_call(ctx, wrapper_label, array0, array1, env_bytes, runtime_label) + }, + )?; + } + _ if descriptor_callback_local_without_same_block_store(ctx, callback)? => { + lower_descriptor_callback_runtime( + ctx, + callback, + visible_arg_types.clone(), + result_elem_ty.clone(), + |ctx, wrapper_label, env_bytes| { + emit_array_map2_call(ctx, wrapper_label, array0, array1, env_bytes, runtime_label) + }, + )?; + } + _ => { + let binding = static_sort_callback_binding( + ctx, + callback, + "array_map callback", + Some(&visible_arg_types), + )?; + let env_bytes = reserve_static_callback_env(ctx, binding.env_source)?; + emit_array_map2_call(ctx, &binding.label, array0, array1, env_bytes, runtime_label)?; + if env_bytes != 0 { + abi::emit_release_temporary_stack(ctx.emitter, env_bytes); + } + } + } + normalize_indexed_array_result(ctx, "array_map", &result_elem_ty, &result_elem_ty)?; + box_array_result_for_mixed_builtin(ctx, inst, &result_elem_ty); + store_if_result(ctx, inst) +} + +/// Emits the runtime call for a two-input-array `array_map`, loading the callback wrapper, both +/// source arrays, and the captured-environment pointer into the first four integer argument +/// registers before branching to the selected `__rt_array_map2*` helper. +fn emit_array_map2_call( + ctx: &mut FunctionContext<'_>, + callback_label: &str, + array0: ValueId, + array1: ValueId, + env_bytes: usize, + runtime_label: &str, +) -> Result<()> { + let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let array0_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + let array1_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let env_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_symbol_address(ctx.emitter, callback_arg_reg, callback_label); + ctx.load_value_to_reg(array0, array0_arg_reg)?; + ctx.load_value_to_reg(array1, array1_arg_reg)?; + load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); + abi::emit_call_label(ctx.emitter, runtime_label); + Ok(()) +} + /// Lowers `array_map()` through a descriptor-backed callback wrapper. fn lower_array_map_descriptor_callback( ctx: &mut FunctionContext<'_>, @@ -851,11 +957,19 @@ pub(crate) fn lower_array_merge(ctx: &mut FunctionContext<'_>, inst: &Instructio super::ensure_arg_count(inst, "array_merge", 2)?; let first = expect_operand(inst, 0)?; let second = expect_operand(inst, 1)?; - let elem_ty = compatible_eight_byte_indexed_array_element_type( - ctx.value_php_type(first)?, - ctx.value_php_type(second)?, - "array_merge", - )?; + let first_ty = ctx.value_php_type(first)?; + let second_ty = ctx.value_php_type(second)?; + // String elements use 16-byte (ptr+len) slots; routing them through the 8-byte helper corrupts + // every element past the first, so a dedicated str helper handles them. An empty `[]` argument + // carries no element-type hint, so the str path triggers whenever EITHER argument is a string + // array (the common `$r = []; array_merge($r, $strs)` shape). + let helper = if is_str_element_array(&first_ty) || is_str_element_array(&second_ty) { + "__rt_array_merge_str" + } else { + let elem_ty = + compatible_eight_byte_indexed_array_element_type(first_ty, second_ty, "array_merge")?; + array_merge_runtime_helper(&elem_ty) + }; match ctx.emitter.target.arch { Arch::AArch64 => { ctx.load_value_to_reg(first, "x0")?; @@ -866,10 +980,18 @@ pub(crate) fn lower_array_merge(ctx: &mut FunctionContext<'_>, inst: &Instructio ctx.load_value_to_reg(second, "rsi")?; } } - abi::emit_call_label(ctx.emitter, array_merge_runtime_helper(&elem_ty)); + abi::emit_call_label(ctx.emitter, helper); store_if_result(ctx, inst) } +/// Returns true when a value's static type is an indexed array of string elements. +/// +/// String indexed arrays use 16-byte (pointer, length) slots, so `array_merge` routes them to the +/// dedicated `__rt_array_merge_str` helper rather than the 8-byte scalar/refcounted merges. +fn is_str_element_array(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Array(elem) if matches!(*elem, PhpType::Str)) +} + /// Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots. pub(crate) fn lower_array_diff(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { lower_indexed_array_set_op( @@ -904,7 +1026,7 @@ pub(crate) fn lower_array_intersect_key(ctx: &mut FunctionContext<'_>, inst: &In /// Lowers `array_slice()` for indexed arrays with pointer-sized payload slots. pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - ensure_arg_count_between(inst, "array_slice", 2, 3)?; + ensure_arg_count_between(inst, "array_slice", 2, 4)?; let array = expect_operand(inst, 0)?; if matches!( ctx.value_php_type(array)?.codegen_repr(), @@ -913,12 +1035,18 @@ pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instructio return lower_mixed_array_slice(ctx, inst); } let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { + let length = if inst.operands.len() >= 3 { Some(expect_operand(inst, 2)?) } else { None }; let source_elem_ty = array_slice_source_element_type(ctx.value_php_type(array)?)?; + // A literal `preserve_keys = true` 4th argument is typed by the checker/EIR as an integer-keyed + // associative array; route it to the key-preserving hash helper instead of the re-indexing path. + if matches!(inst.result_php_type.codegen_repr(), PhpType::AssocArray { .. }) { + lower_array_slice_preserve_call(ctx, array, offset, length, inst)?; + return store_if_result(ctx, inst); + } let result_elem_ty = result_array_element_type("array_slice", &inst.result_php_type.codegen_repr())?; require_array_slice_result_type(&source_elem_ty, &result_elem_ty)?; lower_array_slice_call(ctx, array, offset, length, &source_elem_ty)?; @@ -926,11 +1054,40 @@ pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instructio store_if_result(ctx, inst) } +/// Builds a key-preserving `array_slice()` result through `__rt_array_slice_preserve`. +/// +/// Materializes `(array, offset, length)` exactly like the indexed slice (offset/length are unboxed +/// from `Mixed` cells and an absent length becomes the until-the-end sentinel), then passes the +/// result hash's runtime value-type tag in the 4th argument register so the boxed hash values read +/// back as the correct type. The result is the integer-keyed associative array the checker typed. +fn lower_array_slice_preserve_call( + ctx: &mut FunctionContext<'_>, + array: ValueId, + offset: ValueId, + length: Option, + inst: &Instruction, +) -> Result<()> { + lower_slice_like_args(ctx, array, offset, length, "array_slice")?; + let value_ty = match inst.result_php_type.codegen_repr() { + PhpType::AssocArray { value, .. } => value.codegen_repr(), + other => { + return Err(CodegenIrError::unsupported(format!( + "array_slice preserve_keys result PHP type {:?}", + other + ))) + } + }; + let value_tag = crate::codegen::runtime_value_tag(&value_ty) as i64; + abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 3), value_tag); + abi::emit_call_label(ctx.emitter, "__rt_array_slice_preserve"); + Ok(()) +} + /// Lowers `array_slice()` for an indexed array stored inside a boxed Mixed cell. fn lower_mixed_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let array = expect_operand(inst, 0)?; let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { + let length = if inst.operands.len() >= 3 { Some(expect_operand(inst, 2)?) } else { None @@ -1074,22 +1231,36 @@ pub(crate) fn lower_array_unshift(ctx: &mut FunctionContext<'_>, inst: &Instruct /// Lowers `sort()` for indexed integer arrays by mutating the source array in place. pub(crate) fn lower_sort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "sort", "__rt_sort_int", Some("__rt_sort_str")) + lower_indexed_array_sort( + ctx, + inst, + "sort", + "__rt_sort_int", + Some("__rt_sort_str"), + Some("__rt_sort_float"), + ) } /// Lowers `rsort()` for indexed integer arrays by mutating the source array in place. pub(crate) fn lower_rsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "rsort", "__rt_rsort_int", Some("__rt_rsort_str")) + lower_indexed_array_sort( + ctx, + inst, + "rsort", + "__rt_rsort_int", + Some("__rt_rsort_str"), + Some("__rt_rsort_float"), + ) } /// Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper. pub(crate) fn lower_asort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "asort", "__rt_asort", None) + lower_indexed_array_sort(ctx, inst, "asort", "__rt_asort", None, None) } /// Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper. pub(crate) fn lower_arsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "arsort", "__rt_arsort", None) + lower_indexed_array_sort(ctx, inst, "arsort", "__rt_arsort", None, None) } /// Lowers `ksort()` through the legacy key-sort helper surface. @@ -1104,12 +1275,12 @@ pub(crate) fn lower_krsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> /// Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper. pub(crate) fn lower_natsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "natsort", "__rt_natsort", None) + lower_indexed_array_sort(ctx, inst, "natsort", "__rt_natsort", None, None) } /// Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper. pub(crate) fn lower_natcasesort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_indexed_array_sort(ctx, inst, "natcasesort", "__rt_natcasesort", None) + lower_indexed_array_sort(ctx, inst, "natcasesort", "__rt_natcasesort", None, None) } /// Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place. @@ -1708,7 +1879,9 @@ pub(crate) fn lower_array_multisort(ctx: &mut FunctionContext<'_>, inst: &Instru /// Lowers `array_search()` for indexed arrays with integer-like payloads. pub(crate) fn lower_array_search(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "array_search", 2)?; + // The optional 3rd `$strict` operand is already evaluated during EIR arg lowering; elephc's + // same-type comparison is strict-equivalent, so the backend ignores the flag value here. + ensure_arg_count_between(inst, "array_search", 2, 3)?; let needle = expect_operand(inst, 0)?; let array = expect_operand(inst, 1)?; let needle_ty = ctx.value_php_type(needle)?; @@ -1727,7 +1900,9 @@ pub(crate) fn lower_array_search(ctx: &mut FunctionContext<'_>, inst: &Instructi /// Lowers `in_array()` for indexed arrays with scalar or string payloads. pub(crate) fn lower_in_array(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "in_array", 2)?; + // The optional 3rd `$strict` operand is already evaluated (for side effects) during EIR arg + // lowering; elephc's same-type comparison is strict-equivalent, so the backend ignores it. + ensure_arg_count_between(inst, "in_array", 2, 3)?; let needle = expect_operand(inst, 0)?; let array = expect_operand(inst, 1)?; let needle_ty = ctx.value_php_type(needle)?; @@ -1752,15 +1927,23 @@ fn lower_indexed_array_aggregate( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, - helper: &str, + int_helper: &str, + float_helper: &str, ) -> Result<()> { super::ensure_arg_count(inst, name, 1)?; let array = expect_operand(inst, 0)?; - require_supported_indexed_array(ctx.value_php_type(array)?, name)?; + let elem_ty = aggregate_indexed_array_element_type(ctx.value_php_type(array)?, name)?; ctx.load_value_to_result(array)?; if ctx.emitter.target.arch == Arch::X86_64 { ctx.emitter.instruction("mov rdi, rax"); // pass the indexed-array pointer as the runtime helper argument } + // Float payloads accumulate with floating-point adds/muls and return in the float result + // register (`d0`/`xmm0`); integer-like payloads use the raw-word helper returning `x0`/`rax`. + let helper = if matches!(elem_ty, PhpType::Float) { + float_helper + } else { + int_helper + }; abi::emit_call_label(ctx.emitter, helper); store_if_result(ctx, inst) } @@ -1838,10 +2021,16 @@ fn lower_indexed_array_sort( name: &str, int_helper: &str, str_helper: Option<&str>, + float_helper: Option<&str>, ) -> Result<()> { super::ensure_arg_count(inst, name, 1)?; let array = expect_operand(inst, 0)?; - let elem_ty = indexed_sort_element_type(ctx.value_php_type(array)?, name, str_helper.is_some())?; + let elem_ty = indexed_sort_element_type( + ctx.value_php_type(array)?, + name, + str_helper.is_some(), + float_helper.is_some(), + )?; let source_local = source_load_local_slot(ctx, array)?; ensure_unique_sort_source(ctx, array)?; if let Some(slot) = source_local { @@ -1857,6 +2046,8 @@ fn lower_indexed_array_sort( } let helper = if elem_ty == PhpType::Str { str_helper.expect("string sort helper is required after validation") + } else if elem_ty == PhpType::Float { + float_helper.expect("float sort helper is required after validation") } else { int_helper }; @@ -2036,12 +2227,14 @@ fn indexed_sort_element_type( ty: PhpType, name: &str, allow_strings: bool, + allow_floats: bool, ) -> Result { match ty.codegen_repr() { PhpType::Array(elem) => { let elem = elem.codegen_repr(); if matches!(elem, PhpType::Int | PhpType::Void | PhpType::Never) || (allow_strings && elem == PhpType::Str) + || (allow_floats && elem == PhpType::Float) { return Ok(elem); } @@ -2105,10 +2298,20 @@ fn ensure_unique_sort_source(ctx: &mut FunctionContext<'_>, array: ValueId) -> R ctx.store_result_value(array) } -/// Verifies the aggregate can use the current raw integer-slot runtime helper. -fn require_supported_indexed_array(ty: PhpType, name: &str) -> Result<()> { +/// Returns the aggregate element type routed to a raw-word or floating-point runtime helper. +/// +/// Integer/bool/never element arrays sum through the raw-word helper; float element arrays sum +/// through the floating-point helper. Any other payload is unsupported and errors loudly. +fn aggregate_indexed_array_element_type(ty: PhpType, name: &str) -> Result { match ty.codegen_repr() { - PhpType::Array(elem) if matches!(*elem, PhpType::Int | PhpType::Bool | PhpType::Never) => Ok(()), + PhpType::Array(elem) + if matches!( + *elem, + PhpType::Int | PhpType::Bool | PhpType::Never | PhpType::Float + ) => + { + Ok((*elem).clone()) + } other => Err(CodegenIrError::unsupported(format!( "{} for PHP type {:?}", name, diff --git a/src/codegen_ir/lower_inst/builtins/math.rs b/src/codegen_ir/lower_inst/builtins/math.rs index 8c5563b38f..38d90bd2a2 100644 --- a/src/codegen_ir/lower_inst/builtins/math.rs +++ b/src/codegen_ir/lower_inst/builtins/math.rs @@ -11,7 +11,7 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; -use crate::ir::Instruction; +use crate::ir::{Immediate, Instruction}; use crate::types::PhpType; use crate::codegen_ir::{CodegenIrError, Result}; @@ -184,22 +184,46 @@ pub(crate) fn lower_is_finite(ctx: &mut FunctionContext<'_>, inst: &Instruction) /// Lowers `round()` for concrete integer-like and floating operands. pub(crate) fn lower_round(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - if inst.operands.is_empty() || inst.operands.len() > 2 { + if inst.operands.is_empty() || inst.operands.len() > 3 { return Err(CodegenIrError::invalid_module(format!( - "round expected 1 or 2 args, got {}", + "round expected 1 to 3 args, got {}", inst.operands.len() ))); } + let half_even = round_mode_is_half_even(ctx, inst); let value = expect_operand(inst, 0)?; load_numeric_as_float(ctx, value, "round")?; if inst.operands.len() == 1 { - emit_round_loaded_float(ctx); + emit_round_loaded_float(ctx, half_even); } else { - emit_round_loaded_float_with_precision(ctx, inst)?; + emit_round_loaded_float_with_precision(ctx, inst, half_even)?; } store_if_result(ctx, inst) } +/// Returns whether the optional 3rd `round()` mode operand statically selects `PHP_ROUND_HALF_EVEN`. +/// +/// The mode is resolved at compile time from a constant operand (`PHP_ROUND_HALF_EVEN` lowers to the +/// integer literal `3`). A runtime or absent mode — and the only other checker-accepted value, +/// `PHP_ROUND_HALF_UP` — uses the default ties-away-from-zero rounding, matching the legacy backend. +fn round_mode_is_half_even(ctx: &FunctionContext<'_>, inst: &Instruction) -> bool { + let Some(mode) = inst.operands.get(2) else { + return false; + }; + let Some(value) = ctx.function.value(*mode) else { + return false; + }; + let crate::ir::ValueDef::Instruction { inst: def_inst, .. } = &value.def else { + return false; + }; + matches!( + ctx.function + .instruction(*def_inst) + .and_then(|def| def.immediate.as_ref()), + Some(Immediate::I64(3)) + ) +} + /// Lowers numeric `min()` and `max()` over concrete integer-like or float operands. pub(crate) fn lower_min_max( ctx: &mut FunctionContext<'_>, @@ -222,8 +246,11 @@ pub(crate) fn lower_min_max( PhpType::Float => lower_float_min_max(ctx, inst, want_max)?, PhpType::Int | PhpType::Bool => lower_int_min_max(ctx, inst, want_max)?, PhpType::Mixed | PhpType::Union(_) => { - lower_int_min_max(ctx, inst, want_max)?; - crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + // A Mixed/Union operand holds a boxed cell, not a comparable scalar; compare in float + // space (each operand unboxes through `__rt_mixed_cast_float` in `load_numeric_as_float`) + // and box the winning value as a float so the numeric value is used, not the pointer. + lower_float_min_max(ctx, inst, want_max)?; + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Float); } other => { return Err(CodegenIrError::unsupported(format!( @@ -703,14 +730,25 @@ fn lower_float_rounding_builtin( store_if_result(ctx, inst) } -/// Rounds the loaded float to the nearest integer using PHP's ties-away behavior. -fn emit_round_loaded_float(ctx: &mut FunctionContext<'_>) { +/// Rounds the loaded float to the nearest integer. +/// +/// `half_even` selects PHP_ROUND_HALF_EVEN (round to nearest, ties to even); otherwise PHP's +/// default ties-away-from-zero behavior is used. +fn emit_round_loaded_float(ctx: &mut FunctionContext<'_>, half_even: bool) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("frinta d0, d0"); // round to nearest with ties away from zero + if half_even { + ctx.emitter.instruction("frintn d0, d0"); // PHP_ROUND_HALF_EVEN: round to nearest, ties to even + } else { + ctx.emitter.instruction("frinta d0, d0"); // round to nearest with ties away from zero + } } Arch::X86_64 => { - ctx.emitter.bl_c("round"); + if half_even { + ctx.emitter.bl_c("rint"); + } else { + ctx.emitter.bl_c("round"); + } } } } @@ -719,6 +757,7 @@ fn emit_round_loaded_float(ctx: &mut FunctionContext<'_>) { fn emit_round_loaded_float_with_precision( ctx: &mut FunctionContext<'_>, inst: &Instruction, + half_even: bool, ) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { @@ -733,7 +772,11 @@ fn emit_round_loaded_float_with_precision( ctx.emitter.instruction("ldr d1, [sp], #16"); // restore the original value after pow() returns the multiplier ctx.emitter.instruction("fmul d1, d1, d0"); // scale the original value by the precision multiplier ctx.emitter.instruction("str d0, [sp, #-16]!"); // preserve the multiplier for the final division - ctx.emitter.instruction("frinta d0, d1"); // round the scaled value with ties away from zero + if half_even { + ctx.emitter.instruction("frintn d0, d1"); // PHP_ROUND_HALF_EVEN: round the scaled value to nearest, ties to even + } else { + ctx.emitter.instruction("frinta d0, d1"); // round the scaled value with ties away from zero + } ctx.emitter.instruction("ldr d1, [sp], #16"); // restore the precision multiplier for rescaling ctx.emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to the requested precision } @@ -749,7 +792,11 @@ fn emit_round_loaded_float_with_precision( ctx.emitter.instruction("mulsd xmm1, xmm0"); // scale the original value by the precision multiplier abi::emit_push_float_reg(ctx.emitter, "xmm0"); ctx.emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round() argument register - ctx.emitter.bl_c("round"); + if half_even { + ctx.emitter.bl_c("rint"); + } else { + ctx.emitter.bl_c("round"); + } abi::emit_pop_float_reg(ctx.emitter, "xmm1"); ctx.emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to the requested precision } diff --git a/src/codegen_ir/lower_inst/builtins/strings.rs b/src/codegen_ir/lower_inst/builtins/strings.rs index 6e5465c622..b17fb1a965 100644 --- a/src/codegen_ir/lower_inst/builtins/strings.rs +++ b/src/codegen_ir/lower_inst/builtins/strings.rs @@ -718,10 +718,9 @@ pub(crate) fn lower_substr(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> ))); } let neg_done = ctx.next_label("substr_neg_done"); - let len_done = ctx.next_label("substr_len_done"); match ctx.emitter.target.arch { - Arch::AArch64 => lower_substr_aarch64(ctx, inst, &neg_done, &len_done)?, - Arch::X86_64 => lower_substr_x86_64(ctx, inst, &neg_done, &len_done)?, + Arch::AArch64 => lower_substr_aarch64(ctx, inst, &neg_done)?, + Arch::X86_64 => lower_substr_x86_64(ctx, inst, &neg_done)?, } store_if_result(ctx, inst) } @@ -888,8 +887,8 @@ pub(crate) fn lower_number_format( abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); push_decimal_count(ctx, inst)?; - push_separator_byte(ctx, inst, 2, 46, false, "decimal separator")?; - push_separator_byte(ctx, inst, 3, 44, true, "thousands separator")?; + push_number_format_separator(ctx, inst, 2, b".", "decimal separator")?; + push_number_format_separator(ctx, inst, 3, b",", "thousands separator")?; pop_number_format_args(ctx); abi::emit_call_label(ctx.emitter, "__rt_number_format"); store_if_result(ctx, inst) @@ -1436,15 +1435,15 @@ fn lower_substr_aarch64( ctx: &mut FunctionContext<'_>, inst: &Instruction, neg_done: &str, - len_done: &str, ) -> Result<()> { load_substr_string_and_offset_aarch64(ctx, inst)?; - if inst.operands.len() >= 3 { + // Whether a length argument was supplied is known at compile time, so no runtime sentinel is + // needed: an omitted length keeps the whole remaining tail, an explicit length is clamped below. + let has_length = inst.operands.len() >= 3; + if has_length { let length = expect_operand(inst, 2)?; load_as_int(ctx, length, "substr length")?; ctx.emitter.instruction("mov x3, x0"); // move the explicit substring length into the clamp register - } else { - ctx.emitter.instruction("mov x3, #-1"); // use -1 as the sentinel for an omitted substring length } ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the substring offset after optional length materialization ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the source string pointer and length @@ -1458,13 +1457,15 @@ fn lower_substr_aarch64( ctx.emitter.instruction("csel x0, x2, x0, gt"); // clamp offsets past the end to the source-string length ctx.emitter.instruction("add x1, x1, x0"); // advance the result pointer to the selected substring start ctx.emitter.instruction("sub x2, x2, x0"); // compute the remaining byte length after the selected offset - ctx.emitter.instruction("cmn x3, #1"); // check whether the optional length argument was omitted - ctx.emitter.instruction(&format!("b.eq {}", len_done)); // keep the full remaining tail when no explicit length was provided - ctx.emitter.instruction("cmp x3, #0"); // check whether the requested substring length is negative - ctx.emitter.instruction("csel x3, xzr, x3, lt"); // clamp negative requested lengths to zero - ctx.emitter.instruction("cmp x3, x2"); // compare requested length against the remaining tail length - ctx.emitter.instruction("csel x2, x3, x2, lt"); // shrink the result length when the requested length is shorter - ctx.emitter.label(len_done); + if has_length { + ctx.emitter.instruction("add x4, x2, x3"); // a negative length keeps remaining + length characters + ctx.emitter.instruction("cmp x3, #0"); // check whether the requested substring length is negative + ctx.emitter.instruction("csel x4, x3, x4, ge"); // a non-negative length uses the requested length directly + ctx.emitter.instruction("cmp x4, #0"); // check whether the effective length underflowed below zero + ctx.emitter.instruction("csel x4, xzr, x4, lt"); // clamp the underflowed length up to zero + ctx.emitter.instruction("cmp x4, x2"); // compare the effective length against the remaining tail + ctx.emitter.instruction("csel x2, x4, x2, lt"); // keep min(effective length, remaining tail) + } Ok(()) } @@ -1486,15 +1487,15 @@ fn lower_substr_x86_64( ctx: &mut FunctionContext<'_>, inst: &Instruction, neg_done: &str, - len_done: &str, ) -> Result<()> { load_substr_string_and_offset_x86_64(ctx, inst)?; - if inst.operands.len() >= 3 { + // Whether a length argument was supplied is known at compile time, so no runtime sentinel is + // needed: an omitted length keeps the whole remaining tail, an explicit length is clamped below. + let has_length = inst.operands.len() >= 3; + if has_length { let length = expect_operand(inst, 2)?; load_as_int(ctx, length, "substr length")?; ctx.emitter.instruction("mov rcx, rax"); // move the explicit substring length into the clamp register - } else { - abi::emit_load_int_immediate(ctx.emitter, "rcx", -1); } abi::emit_pop_reg(ctx.emitter, "rax"); abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); @@ -1509,14 +1510,17 @@ fn lower_substr_x86_64( ctx.emitter.instruction("cmovg rax, rsi"); // clamp offsets past the end to the source-string length ctx.emitter.instruction("add rdi, rax"); // advance the result pointer to the selected substring start ctx.emitter.instruction("sub rsi, rax"); // compute the remaining byte length after the selected offset - ctx.emitter.instruction("cmp rcx, -1"); // check whether the optional length argument was omitted - ctx.emitter.instruction(&format!("je {}", len_done)); // keep the full remaining tail when no explicit length was provided - ctx.emitter.instruction("cmp rcx, 0"); // check whether the requested substring length is negative - ctx.emitter.instruction("mov r8, 0"); // materialize zero for negative length clamping - ctx.emitter.instruction("cmovl rcx, r8"); // clamp negative requested lengths to zero - ctx.emitter.instruction("cmp rcx, rsi"); // compare requested length against the remaining tail length - ctx.emitter.instruction("cmovl rsi, rcx"); // shrink the result length when the requested length is shorter - ctx.emitter.label(len_done); + if has_length { + ctx.emitter.instruction("mov r8, rsi"); // r8 = remaining tail length + ctx.emitter.instruction("add r8, rcx"); // a negative length keeps remaining + length characters + ctx.emitter.instruction("cmp rcx, 0"); // check whether the requested substring length is negative + ctx.emitter.instruction("cmovge r8, rcx"); // a non-negative length uses the requested length directly + ctx.emitter.instruction("mov r9, 0"); // materialize zero for the underflow clamp + ctx.emitter.instruction("cmp r8, 0"); // check whether the effective length underflowed below zero + ctx.emitter.instruction("cmovl r8, r9"); // clamp the underflowed length up to zero + ctx.emitter.instruction("cmp r8, rsi"); // compare the effective length against the remaining tail + ctx.emitter.instruction("cmovl rsi, r8"); // keep min(effective length, remaining tail) + } ctx.emitter.instruction("mov rax, rdi"); // return the selected substring pointer in the string result register ctx.emitter.instruction("mov rdx, rsi"); // return the selected substring length in the string result register Ok(()) @@ -3038,94 +3042,58 @@ fn push_decimal_count(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resu Ok(()) } -/// Pushes a one-byte separator argument, using `default_byte` when it is omitted. -fn push_separator_byte( +/// Pushes a `number_format` separator argument as a `(pointer, length)` pair (pointer first, +/// then length) so multi-byte separators (e.g. a UTF-8 non-breaking space) survive intact. +/// +/// An omitted argument falls back to the interned `default` bytes (`"."` / `","`); an explicit +/// empty separator is passed through with length 0, which `__rt_number_format` treats as +/// "insert no separator", replacing the previous single-byte truncation. +fn push_number_format_separator( ctx: &mut FunctionContext<'_>, inst: &Instruction, operand_index: usize, - default_byte: i64, - empty_string_means_zero: bool, + default: &[u8], name: &str, ) -> Result<()> { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); if inst.operands.len() > operand_index { let value = expect_operand(inst, operand_index)?; - load_separator_byte(ctx, value, empty_string_means_zero, name)?; - } else { - abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), default_byte); - } - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - Ok(()) -} - -/// Loads the first byte of a separator string into the integer result register. -fn load_separator_byte( - ctx: &mut FunctionContext<'_>, - value: ValueId, - empty_string_means_zero: bool, - name: &str, -) -> Result<()> { - if ctx.value_php_type(value)? != PhpType::Str { - return Err(CodegenIrError::unsupported(format!( - "number_format {} for non-string operand", - name - ))); - } - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.load_string_value_to_regs(value, "x1", "x2")?; - if empty_string_means_zero { - emit_aarch64_empty_separator_guard(ctx); - } else { - ctx.emitter.instruction("ldrb w0, [x1]"); // load the first byte of the separator string - } - } - Arch::X86_64 => { - ctx.load_string_value_to_regs(value, "rax", "rdx")?; - if empty_string_means_zero { - emit_x86_64_empty_separator_guard(ctx); - } else { - ctx.emitter.instruction("movzx eax, BYTE PTR [rax]"); // load the first byte of the separator string - } + if ctx.value_php_type(value)? != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "number_format {} for non-string operand", + name + ))); } + ctx.load_string_value_to_regs(value, ptr_reg, len_reg)?; + } else { + let (label, _) = ctx.data.add_string(default); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, default.len() as i64); } + abi::emit_push_reg(ctx.emitter, ptr_reg); + abi::emit_push_reg(ctx.emitter, len_reg); Ok(()) } -/// Emits the AArch64 empty-string fallback for the optional thousands separator. -fn emit_aarch64_empty_separator_guard(ctx: &mut FunctionContext<'_>) { - let use_zero = ctx.next_label("nf_sep_zero"); - let done = ctx.next_label("nf_sep_done"); - ctx.emitter.instruction(&format!("cbz x2, {}", use_zero)); // use the no-separator sentinel when the separator string is empty - ctx.emitter.instruction("ldrb w0, [x1]"); // load the first byte of the non-empty separator string - ctx.emitter.instruction(&format!("b {}", done)); // skip the empty-string separator fallback - ctx.emitter.label(&use_zero); - abi::emit_load_int_immediate(ctx.emitter, "x0", 0); - ctx.emitter.label(&done); -} - -/// Emits the x86_64 empty-string fallback for the optional thousands separator. -fn emit_x86_64_empty_separator_guard(ctx: &mut FunctionContext<'_>) { - let use_zero = ctx.next_label("nf_sep_zero"); - let done = ctx.next_label("nf_sep_done"); - ctx.emitter.instruction("test rdx, rdx"); // check whether the separator string is empty - ctx.emitter.instruction(&format!("jz {}", use_zero)); // use the no-separator sentinel for an empty separator - ctx.emitter.instruction("movzx eax, BYTE PTR [rax]"); // load the first byte of the non-empty separator string - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the empty-string separator fallback - ctx.emitter.label(&use_zero); - abi::emit_load_int_immediate(ctx.emitter, "rax", 0); - ctx.emitter.label(&done); -} - /// Pops the staged arguments into the runtime helper's target ABI registers. +/// +/// ABI: number in `d0`/`xmm0`, decimals in `x1`/`rdi`, decimal separator `(ptr, len)` in +/// `x2`/`x3` (`rsi`/`rdx`), thousands separator `(ptr, len)` in `x4`/`x5` (`rcx`/`r8`). The +/// staged push order is number, decimals, dec ptr, dec len, thousands ptr, thousands len, so +/// the pops run in reverse. fn pop_number_format_args(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x5"); + abi::emit_pop_reg(ctx.emitter, "x4"); abi::emit_pop_reg(ctx.emitter, "x3"); abi::emit_pop_reg(ctx.emitter, "x2"); abi::emit_pop_reg(ctx.emitter, "x1"); abi::emit_pop_float_reg(ctx.emitter, "d0"); } Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "r8"); + abi::emit_pop_reg(ctx.emitter, "rcx"); abi::emit_pop_reg(ctx.emitter, "rdx"); abi::emit_pop_reg(ctx.emitter, "rsi"); abi::emit_pop_reg(ctx.emitter, "rdi"); diff --git a/src/codegen_ir/lower_inst/builtins/types.rs b/src/codegen_ir/lower_inst/builtins/types.rs index 04632a340f..76d9c95410 100644 --- a/src/codegen_ir/lower_inst/builtins/types.rs +++ b/src/codegen_ir/lower_inst/builtins/types.rs @@ -365,25 +365,121 @@ pub(crate) fn lower_class_name_lookup( store_if_result(ctx, inst) } -/// Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets. +/// Lowers `is_a()` and `is_subclass_of()` for object, boxed-Mixed, and string-class-name operands. pub(crate) fn lower_is_a_relation( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, ) -> Result<()> { super::ensure_arg_count_between(inst, name, 2, 3)?; + let object = expect_operand(inst, 0)?; + let target = expect_operand(inst, 1)?; + let exclude_self = name == "is_subclass_of"; + + // Runtime path: a boxed Mixed/Union receiver with a literal target class. The relation cannot be + // folded because the runtime class is unknown statically, so compute the compile-time set of + // class ids that satisfy the relation and compare the unboxed receiver's header class id against + // it. Resolved before the operand-materialization loop because it borrows `ctx` immutably. + let runtime_ids = if matches!(ctx.value_php_type(object)?, PhpType::Mixed | PhpType::Union(_)) { + optional_const_string_operand(ctx, target)? + .map(|target_class| matching_class_ids(ctx, &target_class, exclude_self)) + } else { + None + }; + for value in &inst.operands { ctx.load_value_to_result(*value)?; } - let object = expect_operand(inst, 0)?; - let target = expect_operand(inst, 1)?; - let exclude_self = name == "is_subclass_of"; - let result = static_relation_holds(ctx, object, target, exclude_self)?; + if let Some(ids) = runtime_ids { + ctx.load_value_to_result(object)?; + emit_runtime_class_is_a(ctx, &ids); + return store_if_result(ctx, inst); + } + + let result = static_relation_holds(ctx, inst, object, target, exclude_self)?; emit_bool_result(ctx, result); store_if_result(ctx, inst) } +/// Returns the class ids of every known class that satisfies `is_a(_, target)` (the target class +/// itself, a subclass, or an implementer), excluding the target's own id when `exclude_self` is set +/// (the `is_subclass_of` semantics). Ids are sorted and deduplicated for deterministic emission. +fn matching_class_ids(ctx: &FunctionContext<'_>, target: &str, exclude_self: bool) -> Vec { + let target_key = php_symbol_key(target.trim_start_matches('\\')); + let mut ids: Vec = ctx + .module + .class_infos + .iter() + .filter(|(class_name, _)| { + class_satisfies_relation(ctx, class_name.trim_start_matches('\\'), &target_key, exclude_self) + }) + .map(|(_, info)| info.class_id) + .collect(); + ids.sort_unstable(); + ids.dedup(); + ids +} + +/// Returns true when `class_name` satisfies the relation to the target symbol key: the same class +/// (unless `exclude_self`), a subclass via the parent chain, or an implementer of the interface. +fn class_satisfies_relation( + ctx: &FunctionContext<'_>, + class_name: &str, + target_key: &str, + exclude_self: bool, +) -> bool { + if !exclude_self && php_symbol_key(class_name) == target_key { + return true; + } + parent_chain_contains(ctx, class_name, target_key) + || class_interfaces_contain(ctx, class_name, target_key) +} + +/// Emits the runtime class-relation check for a boxed receiver already in the integer result +/// register: unboxes it, returns `0` for a non-object payload, otherwise compares the header class +/// id against each id in `ids` (the compile-time set satisfying the relation) and leaves `1`/`0`. +fn emit_runtime_class_is_a(ctx: &mut FunctionContext<'_>, ids: &[u64]) { + let no_match = ctx.next_label("is_a_no"); + let matched = ctx.next_label("is_a_yes"); + let done = ctx.next_label("is_a_done"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the receiver is an object + ctx.emitter.instruction(&format!("b.ne {}", no_match)); // a non-object receiver is never is-a a class + ctx.emitter.instruction("ldr x9, [x1]"); // load the receiver's class id from the object header + for id in ids { + abi::emit_load_int_immediate(ctx.emitter, "x10", *id as i64); + ctx.emitter.instruction("cmp x9, x10"); // compare the receiver class id against the candidate + ctx.emitter.instruction(&format!("b.eq {}", matched)); // matched: the relation holds + } + ctx.emitter.label(&no_match); + ctx.emitter.instruction("mov x0, #0"); // relation does not hold + ctx.emitter.instruction(&format!("b {}", done)); // skip the matched path + ctx.emitter.label(&matched); + ctx.emitter.instruction("mov x0, #1"); // relation holds + ctx.emitter.label(&done); + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the receiver is an object + ctx.emitter.instruction(&format!("jne {}", no_match)); // a non-object receiver is never is-a a class + ctx.emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the receiver's class id from the object header + for id in ids { + abi::emit_load_int_immediate(ctx.emitter, "r10", *id as i64); + ctx.emitter.instruction("cmp r9, r10"); // compare the receiver class id against the candidate + ctx.emitter.instruction(&format!("je {}", matched)); // matched: the relation holds + } + ctx.emitter.label(&no_match); + ctx.emitter.instruction("xor rax, rax"); // relation does not hold + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the matched path + ctx.emitter.label(&matched); + ctx.emitter.instruction("mov rax, 1"); // relation holds + ctx.emitter.label(&done); + } + } +} + /// Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry. pub(crate) fn lower_get_declared_names( ctx: &mut FunctionContext<'_>, @@ -545,26 +641,77 @@ fn emit_bool_result(ctx: &mut FunctionContext<'_>, value: bool) { /// Statically evaluates an object/class relation against a literal target class name. fn static_relation_holds( ctx: &FunctionContext<'_>, + inst: &Instruction, object: ValueId, target: ValueId, exclude_self: bool, ) -> Result { - let PhpType::Object(object_class) = ctx.value_php_type(object)? else { - return Ok(false); - }; let Some(target_class) = optional_const_string_operand(ctx, target)? else { return Ok(false); }; - let object_class = object_class.trim_start_matches('\\'); + // The subject is either an object receiver (use its static class) or a constant string class + // name. `is_subclass_of()` always accepts a string subject; `is_a()` accepts one only with a + // literal `allow_string = true` third argument (PHP defaults it to false). + let subject_class = match ctx.value_php_type(object)? { + PhpType::Object(object_class) => object_class, + _ => { + let Some(class_name) = optional_const_string_operand(ctx, object)? else { + return Ok(false); + }; + let allow_string = exclude_self + || inst + .operands + .get(2) + .copied() + .map(|allow| optional_const_bool_operand(ctx, allow)) + .transpose()? + .flatten() + == Some(true); + if !allow_string { + return Ok(false); + } + // A string subject only satisfies the relation when the named class is actually declared. + if lookup_class(ctx, class_name.trim_start_matches('\\')).is_none() { + return Ok(false); + } + class_name + } + }; + let subject_class = subject_class.trim_start_matches('\\'); let target_class = target_class.trim_start_matches('\\'); let target_key = php_symbol_key(target_class); - if !exclude_self && php_symbol_key(object_class) == target_key { + if !exclude_self && php_symbol_key(subject_class) == target_key { return Ok(true); } - if parent_chain_contains(ctx, object_class, &target_key) { + if parent_chain_contains(ctx, subject_class, &target_key) { return Ok(true); } - Ok(class_interfaces_contain(ctx, object_class, &target_key)) + Ok(class_interfaces_contain(ctx, subject_class, &target_key)) +} + +/// Returns `Some(bool)` when `value` is a constant boolean EIR operand, else `None`. +/// +/// Used to read a literal `is_a(..., allow_string)` third argument at compile time; a non-literal +/// (runtime) flag yields `None` so the caller conservatively treats a string subject as disallowed. +fn optional_const_bool_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(None); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + if inst_ref.op != Op::ConstBool { + return Ok(None); + } + match inst_ref.immediate { + Some(Immediate::Bool(value)) => Ok(Some(value)), + _ => Ok(None), + } } /// Returns true when an object's parent chain contains the target PHP symbol key. diff --git a/src/codegen_ir/lower_inst/floats.rs b/src/codegen_ir/lower_inst/floats.rs index fb08712097..bed7af3247 100644 --- a/src/codegen_ir/lower_inst/floats.rs +++ b/src/codegen_ir/lower_inst/floats.rs @@ -121,6 +121,45 @@ pub(super) fn lower_float_binop( store_if_result(ctx, inst) } +/// Lowers PHP float `/` with a zero-divisor guard that raises an uncatchable fatal. +/// Mirrors the legacy backend's `emit_float_div_by_zero_guard`: an exact `0.0` divisor fatals +/// (PHP raises a catchable `DivisionByZeroError`; elephc has no exception unwinding yet), while a +/// NaN divisor is left alone (`x / NaN = NaN`), so on x86_64 a parity check skips the fatal for +/// the unordered case and on AArch64 `b.ne` naturally skips the unordered (`Z=0`) NaN result. +pub(super) fn lower_float_div(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let lhs = expect_operand(inst, 0)?; + let rhs = expect_operand(inst, 1)?; + let lhs_reg = secondary_float_reg(ctx.emitter.target.arch); + let rhs_reg = abi::float_result_reg(ctx.emitter); + require_float(ctx.load_value_to_reg(lhs, lhs_reg)?, inst)?; + require_float(ctx.load_value_to_reg(rhs, rhs_reg)?, inst)?; + let ok_label = ctx.next_label("fdiv_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("fcmp {}, #0.0", rhs_reg)); // compare the float divisor against 0.0 + ctx.emitter.instruction(&format!("b.ne {}", ok_label)); // skip the fatal when the divisor is non-zero (or NaN) + } + Arch::X86_64 => { + ctx.emitter.instruction("xorps xmm2, xmm2"); // materialize 0.0 in a scratch register for the comparison + ctx.emitter.instruction(&format!("ucomisd {}, xmm2", rhs_reg)); // compare the float divisor against 0.0 (sets PF when the divisor is NaN) + ctx.emitter.instruction(&format!("jne {}", ok_label)); // skip the fatal when the divisor differs from 0.0 + ctx.emitter.instruction(&format!("jp {}", ok_label)); // skip the fatal when the divisor is NaN (unordered) + } + } + abi::emit_fatal_to_stderr(ctx.emitter, ctx.data, b"Fatal error: division by zero\n"); + ctx.emitter.label(&ok_label); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("fdiv {}, {}, {}", rhs_reg, lhs_reg, rhs_reg)); // compute the floating-point quotient left / right + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("divsd {}, {}", lhs_reg, rhs_reg)); // divide the left float operand by the divisor + ctx.emitter.instruction(&format!("movsd {}, {}", rhs_reg, lhs_reg)); // move the quotient back into the float result register + } + } + store_if_result(ctx, inst) +} + /// Lowers floating-point exponentiation through libc `pow`. pub(super) fn lower_float_pow(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let lhs = expect_operand(inst, 0)?; diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 9983de47f7..d3cdf36349 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1612,12 +1612,65 @@ fn lower_inc_dec( } /// Lowers a direct function, builtin, or extern call. +/// Lowers a 3-or-more-array `array_merge`/`array_diff`/`array_intersect` call by folding the +/// two-argument runtime helper left-associatively (`f(f(a, b), c)`). Left association matches PHP: +/// `merge` concatenates in order, and `diff`/`intersect` compare the first array against every +/// other (`a \ b \ c` and `a ∩ b ∩ c`). +/// +/// Each intermediate result is a freshly allocated, uniquely owned array (these helpers never alias +/// an input into their result), so it is explicitly `Release`d once the next fold step has consumed +/// it — otherwise it would leak (nested array-returning builtin results are not otherwise reclaimed). +/// +/// Returns `None` for the two-argument case, other builtins, or when spread/named arguments prevent +/// a static fold (those fall through to the normal call path, which handles or rejects them). +fn lower_variadic_array_set_op( + ctx: &mut LoweringContext<'_, '_>, + _name: &Name, + canonical: &str, + args: &[Expr], +) -> Option { + if !matches!( + php_symbol_key(canonical.trim_start_matches('\\')).as_str(), + "array_merge" | "array_diff" | "array_intersect" + ) { + return None; + } + if args.len() <= 2 { + return None; + } + if args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_) | ExprKind::NamedArg { .. })) + { + return None; + } + // Evaluate every argument left-to-right (PHP source order) before folding. + let operands: Vec = args.iter().map(|arg| lower_expr(ctx, arg).value).collect(); + let span = args[0].span; + let first_pair = vec![operands[0], operands[1]]; + let first_type = call_return_type(ctx, canonical, &first_pair); + let mut acc = emit_builtin_call_value(ctx, canonical, first_pair, first_type, span); + for &operand in &operands[2..] { + let pair = vec![acc.value, operand]; + let result_type = call_return_type(ctx, canonical, &pair); + let next = emit_builtin_call_value(ctx, canonical, pair, result_type, span); + // The accumulator array has now been consumed by the fold step; release it so the + // intermediate does not leak (the final `acc` is returned and keeps its ownership). + crate::ir_lower::ownership::release_if_owned(ctx, acc, Some(span)); + acc = next; + } + Some(acc) +} + fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[Expr], expr: &Expr) -> LoweredValue { constants::register_static_define_call(ctx, name, args); if let Some(value) = constants::lower_static_defined_call(ctx, name, args, expr) { return value; } let canonical = name.as_str(); + if let Some(value) = lower_variadic_array_set_op(ctx, name, canonical, args) { + return value; + } if let Some(value) = lower_lazy_isset(ctx, canonical, args, expr) { return value; } @@ -5622,12 +5675,42 @@ fn call_return_type_for_args( match php_symbol_key(name.trim_start_matches('\\')).as_str() { "array_fill" => array_fill_builtin_return_type_for_args(ctx, args, operands), "array_map" => array_map_builtin_return_type(ctx, args, operands), + "array_slice" => array_slice_builtin_return_type(ctx, args, operands), "iterator_to_array" => iterator_to_array_builtin_return_type(ctx, args, operands), "microtime" => microtime_builtin_return_type_for_args(args), _ => None, } } +/// Returns the precise `array_slice()` result type when a literal `preserve_keys = true` 4th argument +/// is present over a scalar (int/float/bool) indexed array. +/// +/// The literal-true + scalar-element predicate MUST stay identical to the type checker +/// (`src/builtins/array/array_slice.rs`) and the emitter (`lower_array_slice`): all three must agree +/// on Array-vs-AssocArray or the result heap shape and the value-tag reads back inconsistently. +/// Returns `None` (falling back to the indexed slice type) for every other shape. +fn array_slice_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + operands: &[crate::ir::ValueId], +) -> Option { + if !crate::types::array_slice_literal_preserve_keys(args) { + return None; + } + let elem = match ctx.builder.value_php_type(*operands.first()?).codegen_repr() { + PhpType::Array(inner) + if matches!(*inner, PhpType::Int | PhpType::Float | PhpType::Bool) => + { + *inner + } + _ => return None, + }; + Some(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(elem), + }) +} + /// Returns `microtime()` metadata when the literal `as_float` flag is still available. /// /// `microtime(true)` is a float; `microtime()` / `microtime(false)` is the "0.NNNNNNNN sec" @@ -5677,6 +5760,21 @@ fn array_map_builtin_return_type( args: &[Expr], operands: &[crate::ir::ValueId], ) -> Option { + // The two-input-array form `array_map($cb, $a, $b)` is checker-bounded to integer- or string- + // element arrays whose callback returns that same scalar kind, so the result list element type + // is the shared element type (independent of the callback's possibly-untyped return). + if args.len() == 3 { + let array = operands.get(1)?; + return match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(elem) if matches!(*elem, PhpType::Str) => { + Some(PhpType::Array(Box::new(PhpType::Str))) + } + PhpType::Array(elem) if matches!(*elem, PhpType::Int | PhpType::Bool) => { + Some(PhpType::Array(Box::new(PhpType::Int))) + } + _ => None, + }; + } if args.len() != 2 { return None; } @@ -5794,6 +5892,22 @@ fn numeric_builtin_return_type( let ty = ctx.builder.value_php_type(*value).codegen_repr(); Some(abs_builtin_return_type(&ty)) } + "array_sum" | "array_product" => { + // A float-element array sums/multiplies through the floating-point runtime helper + // and returns a float (matching the EIR backend dispatch); every other element + // kind returns an integer. + let value = operands.first()?; + let float_elements = match ctx.builder.value_php_type(*value).codegen_repr() { + PhpType::Array(elem) => matches!(*elem, PhpType::Float), + PhpType::AssocArray { value, .. } => matches!(*value, PhpType::Float), + _ => false, + }; + Some(if float_elements { + PhpType::Float + } else { + PhpType::Int + }) + } "min" | "max" => { let mut saw_float = false; for value in operands { @@ -6112,11 +6226,18 @@ fn is_empty_array_element_type(ty: &PhpType) -> bool { matches!(ty.codegen_repr(), PhpType::Void) } -/// Returns true for element types copied safely by the scalar merge runtime helper. +/// Returns true for element types the merged array can adopt from the second operand when the +/// first is statically empty. Scalars use the 8-byte merge helper; strings use the dedicated +/// `__rt_array_merge_str` (16-byte slots), so `Str` is included here as well. fn is_scalar_merge_element_type(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Callable | PhpType::Void + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Callable + | PhpType::Void + | PhpType::Str ) } diff --git a/src/optimize/fold/ops.rs b/src/optimize/fold/ops.rs index dba5f1f2f1..313eb9eab2 100644 --- a/src/optimize/fold/ops.rs +++ b/src/optimize/fold/ops.rs @@ -151,13 +151,18 @@ fn fold_int_overflow_to_float(op: &BinOp, left: i64, right: i64) -> Option Option { let (left, right) = (int_literal(left)?, int_literal(right)?); - if right == 0 { - None - } else { - Some(ExprKind::IntLiteral(left % right)) + match right { + 0 => None, + -1 => Some(ExprKind::IntLiteral(0)), + _ => Some(ExprKind::IntLiteral(left % right)), } } diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index faeb153fd7..5f4724696d 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -174,6 +174,158 @@ pub(crate) fn comparator_dummy_arg_for_elem( } } +/// Returns true when a multi-array `array_map()` callback is a supported form. +/// +/// The bounded multi-array path supports a named function (`StringLiteral`), a closure +/// literal (capturing closures and arrow functions included, lowered through the same +/// N-visible-argument wrapper environment used by the single-array path), or a variable +/// that holds a closure (a closure descriptor carries its own captures, so it is invoked +/// directly). Callback forms that select a callable indirectly in another way (a callable +/// `[obj, method]` array, a runtime-string name, or a first-class callable) are deferred to +/// a later increment and rejected with a clear diagnostic. +fn array_map_multi_callback_supported(checker: &Checker, callback: &Expr) -> bool { + match &callback.kind { + ExprKind::StringLiteral(_) | ExprKind::Closure { .. } => true, + // A variable is accepted only when it is known to hold a closure (not an + // `[obj, method]` array, a string function name, or a first-class callable), + // because those other variable forms are materialized through different dispatch + // paths the two-array runtime does not yet drive. + ExprKind::Variable(name) => { + checker.closure_return_types.contains_key(name) + && !checker.callable_array_targets.contains_key(name) + && !checker.first_class_callable_targets.contains_key(name) + } + _ => false, + } +} + +/// Returns true when a multi-array `array_map()` callback over string arrays is supported. +/// +/// String arrays are restricted to a capture-less closure: the captured-closure wrapper path is +/// not yet wired for string elements, and a named function's return type is not statically known +/// here (it defaults to integer), so only a closure — whose string return type is inferred from +/// its body — can be confirmed to produce the strings the runtime collects. +fn array_map_multi_callback_non_capturing(callback: &Expr) -> bool { + matches!( + &callback.kind, + ExprKind::Closure { captures, capture_refs, .. } + if captures.is_empty() && capture_refs.is_empty() + ) +} + +/// Type-checks the multi-array form `array_map($callback, $a, $b, ...)`. +/// +/// Bounded multi-array support: exactly two input arrays that share one scalar element type +/// (both integer, or both string), with a callback that returns that same scalar type. Integer +/// arrays accept any supported callback form (named function, closure including capturing, or a +/// variable holding a closure); string arrays are restricted to a non-capturing callback. Shapes +/// outside this subset (more than two arrays, mixed/other element types, float-returning callbacks, +/// or the `array_map(null, ...)` zip form) are rejected with a clear "not yet supported" diagnostic +/// so they fail at compile time rather than miscompiling. Returns `Array(elem)` for the shared +/// element representation, matching the runtime that collects the results. +pub(crate) fn check_array_map_multi( + checker: &mut Checker, + args: &[Expr], + span: crate::span::Span, + env: &TypeEnv, +) -> Result { + if args.len() != 3 { + return Err(CompileError::new( + span, + "array_map() with more than two input arrays is not yet supported", + )); + } + if matches!(args[0].kind, ExprKind::Null) { + return Err(CompileError::new( + span, + "array_map(null, ...) array zipping is not yet supported", + )); + } + // Collect the input arrays' element types; all must share one scalar representation + // (every array integer, or every array string) because the runtime is chosen per element type. + let mut elem_tys = Vec::new(); + let mut shared_repr: Option = None; + for arr_arg in &args[1..] { + match checker.infer_type(arr_arg, env)? { + PhpType::Array(elem) => { + let repr = elem.codegen_repr(); + if !matches!(repr, PhpType::Int | PhpType::Str) { + return Err(CompileError::new( + span, + "array_map() with multiple arrays currently supports only integer or string arrays", + )); + } + match &shared_repr { + None => shared_repr = Some(repr), + Some(existing) if existing != &repr => { + return Err(CompileError::new( + span, + "array_map() with multiple arrays currently requires every array to share one scalar element type", + )); + } + _ => {} + } + elem_tys.push((*elem).clone()); + } + _ => { + return Err(CompileError::new( + span, + "array_map() arguments after the first must be arrays", + )); + } + } + } + let elem_repr = shared_repr.unwrap_or(PhpType::Int); + + // The callback gate depends on the element type: string arrays allow only a non-capturing + // callback; integer arrays additionally accept capturing closures and closure variables. + let callback_ok = if matches!(elem_repr, PhpType::Str) { + array_map_multi_callback_non_capturing(&args[0]) + } else { + array_map_multi_callback_supported(checker, &args[0]) + }; + if !callback_ok { + let detail = if matches!(elem_repr, PhpType::Str) { + "with multiple string arrays requires a capture-less closure" + } else { + "with multiple arrays currently supports a named function, a closure, or a variable holding a closure" + }; + return Err(CompileError::new( + args[0].span, + &format!("array_map() {detail} as the callback"), + )); + } + + let dummy_args: Vec = elem_tys + .iter() + .map(|et| dummy_arg_for_array_scalar_elem(&PhpType::Array(Box::new(et.clone())), span)) + .collect(); + let callback_ret_ty = check_callback_builtin_call( + checker, + &args[0], + &dummy_args, + span, + env, + "array_map() callback", + )?; + // The runtime collects the result through the result register chosen by the element type, so + // the callback must return that same scalar representation. + if callback_ret_ty.codegen_repr() != elem_repr { + let want = if matches!(elem_repr, PhpType::Str) { + "string" + } else { + "integer" + }; + return Err(CompileError::new( + span, + &format!( + "array_map() with multiple {want} arrays currently requires a {want}-returning callback" + ), + )); + } + Ok(PhpType::Array(Box::new(elem_repr))) +} + /// Checks object or array callable call and reports a compile error when it is invalid. fn check_object_or_array_callable_call( checker: &mut Checker, diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 2a25e0a38d..1180eddc99 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -27,7 +27,7 @@ pub(crate) use catalog::{ }; pub(crate) use callables::{ array_element_type, array_filter_callback_dummy_args, callback_supports_complex_descriptor_env, - check_call_user_func, check_call_user_func_array, + check_array_map_multi, check_call_user_func, check_call_user_func_array, check_callback_builtin_call, check_function_exists, check_preg_replace_callback_first_class_call, comparator_dummy_arg_for_elem, dummy_arg_for_array_scalar_elem, runtime_callable_array_type, diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index eed3237fa7..a6be3fc586 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -16,6 +16,7 @@ use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; +use crate::types::round_constants::ROUND_INT_CONSTANTS; use crate::types::PhpType; use super::super::Checker; @@ -68,6 +69,9 @@ impl Checker { for (name, _value) in DATE_INT_CONSTANTS { constants.insert((*name).to_string(), PhpType::Int); } + for (name, _value) in ROUND_INT_CONSTANTS { + constants.insert((*name).to_string(), PhpType::Int); + } Self { target_platform, diff --git a/src/types/mod.rs b/src/types/mod.rs index 555d823194..dc10129f68 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -32,6 +32,8 @@ mod model; pub(crate) mod preg_constants; /// Type checker result types and the `check` entry point. mod result; +/// PHP `round()` half-mode constants shared by checker and codegen. +pub(crate) mod round_constants; /// Class, interface, enum, and FFI schema definitions. mod schema; /// Function signature representation and builtin signature helpers. @@ -60,6 +62,17 @@ pub(crate) use signatures::{ pub(crate) use signatures::legacy_builtin_call_sig; pub use signatures::FunctionSig; +/// Returns true when an `array_slice()` call has a literal `preserve_keys = true` 4th +/// argument. Shared by the type checker, codegen emitter, and codegen local-type inference so all +/// three agree on the result shape (an integer-keyed associative array). A drift between callers +/// would disagree on Array-vs-AssocArray storage, which is a heap-shape mismatch. +pub(crate) fn array_slice_literal_preserve_keys(args: &[crate::parser::ast::Expr]) -> bool { + matches!( + args.get(3).map(|arg| &arg.kind), + Some(crate::parser::ast::ExprKind::BoolLiteral(true)) + ) +} + /// Type checks the program after name resolution. Returns `CheckResult` with type /// metadata, function/class/interface/enum info, warnings, required libraries, and the /// internal `Mixed` type for heterogeneous assoc-array values. Runs before optimization/codegen. diff --git a/src/types/round_constants.rs b/src/types/round_constants.rs new file mode 100644 index 0000000000..f858499d4d --- /dev/null +++ b/src/types/round_constants.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Defines PHP `PHP_ROUND_HALF_*` rounding-mode integer constants exposed by elephc. +//! Keeps the round-mode values in one source of truth for type checking and codegen. +//! +//! Called from: +//! - `crate::types::checker::driver::init` when registering predefined constants. +//! - `crate::codegen::prescan` when materializing constant literal values. +//! +//! Key details: +//! - Values must match PHP's `round()` mode constants exactly. `round()` currently implements +//! `PHP_ROUND_HALF_UP` (the default, round half away from zero) and `PHP_ROUND_HALF_EVEN` +//! (banker's rounding); `PHP_ROUND_HALF_DOWN` and `PHP_ROUND_HALF_ODD` are recognized constants +//! but rejected with a diagnostic until specialized. + +/// Tuple of `(name, value)` pairs for PHP `round()` rounding-mode integer constants. +pub(crate) const ROUND_INT_CONSTANTS: &[(&str, i64)] = &[ + ("PHP_ROUND_HALF_UP", 1), + ("PHP_ROUND_HALF_DOWN", 2), + ("PHP_ROUND_HALF_EVEN", 3), + ("PHP_ROUND_HALF_ODD", 4), +]; + +/// Returns the rounding-mode value for a `PHP_ROUND_HALF_*` constant name, if recognized. +pub(crate) fn round_mode_value(name: &str) -> Option { + ROUND_INT_CONSTANTS + .iter() + .find(|(candidate, _)| *candidate == name) + .map(|(_, value)| *value) +} diff --git a/src/types/signatures.rs b/src/types/signatures.rs index 7f74e9a770..a3fc7a230a 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -308,9 +308,9 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { "array_pad" => Some(fixed(&["array", "length", "value"])), "array_fill" => Some(fixed(&["start_index", "count", "value"])), "array_slice" => Some(optional( - &["array", "offset", "length"], + &["array", "offset", "length", "preserve_keys"], 2, - vec![null_lit()], + vec![null_lit(), bool_lit(false)], )), "array_splice" => Some(first_param_ref(optional( &["array", "offset", "length"], @@ -353,7 +353,11 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { "clamp" => Some(fixed(&["value", "min", "max"])), "min" | "max" => Some(variadic(&["value"], "values")), "rand" | "mt_rand" | "random_int" => Some(fixed(&["min", "max"])), - "round" => Some(optional(&["num", "precision"], 1, vec![int_lit(0)])), + "round" => Some(optional( + &["num", "precision", "mode"], + 1, + vec![int_lit(0), int_lit(1)], + )), "sleep" => Some(fixed(&["seconds"])), "usleep" => Some(fixed(&["microseconds"])), diff --git a/tests/codegen/arrays/callbacks.rs b/tests/codegen/arrays/callbacks.rs index 2cbdd9adef..3b59ad229d 100644 --- a/tests/codegen/arrays/callbacks.rs +++ b/tests/codegen/arrays/callbacks.rs @@ -26,6 +26,33 @@ echo $b[0] . $b[1] . $b[2]; assert_eq!(out, "246"); } +/// Verifies the two-input-array form `array_map($cb, $a, $b)` pairs elements positionally across +/// every supported integer callback shape (arrow closure, capturing closure, named function) and +/// the string-array form (capture-less closure), routed through `__rt_array_map2`/`_str`. +/// Matches PHP: `11,22,33|110,140,190|9,18,27|ax,by`. +#[test] +fn test_array_map_two_arrays() { + let out = compile_and_run( + r#" $x + $y, $a, $b); +echo $arrow[0]; echo ","; echo $arrow[1]; echo ","; echo $arrow[2]; echo "|"; +$k = 100; +$cap = array_map(function ($x, $y) use ($k) { return $x * $y + $k; }, $a, $b); +echo $cap[0]; echo ","; echo $cap[1]; echo ","; echo $cap[2]; echo "|"; +$named = array_map("combine", $b, $a); +echo $named[0]; echo ","; echo $named[1]; echo ","; echo $named[2]; echo "|"; +$s1 = ["a", "b"]; +$s2 = ["x", "y"]; +$strs = array_map(fn($p, $q) => $p . $q, $s1, $s2); +echo $strs[0]; echo ","; echo $strs[1]; +"#, + ); + assert_eq!(out, "11,22,33|110,140,190|9,18,27|ax,by"); +} + // Tests `array_map` on a single-element array with a user-defined increment callback. /// Verifies that array map single. #[test] diff --git a/tests/codegen/arrays/indexed/aggregates.rs b/tests/codegen/arrays/indexed/aggregates.rs index 0578b73fe8..30ac490358 100644 --- a/tests/codegen/arrays/indexed/aggregates.rs +++ b/tests/codegen/arrays/indexed/aggregates.rs @@ -48,3 +48,20 @@ echo array_product($a); ); assert_eq!(out, "24"); } + +/// Compiles `array_sum($a)`/`array_product($a)` over a float-element array and verifies the +/// floating-point runtime helpers accumulate and return in the float result register. +/// Input: `[1.5, 2.5, 3.5]` → sum = 7.5; `[2.5, 3.0]` → product = 7.5 (matches PHP). +#[test] +fn test_array_sum_and_product_over_float_array() { + let out = compile_and_run( + r#" 3`, `0.5 -> 1`, `-2.5 -> -3`, `2.55 -> 2.6`. +#[test] +fn test_number_format_rounds_half_away_from_zero() { + let out = compile_and_run( + r#" 2.0); assert_eq!(out, "1,1,1,-1"); } +/// Verifies the constant folder computes `i64::MIN % -1` as `0` without panicking the compiler. +/// A direct `left % right` fold would overflow (`i64::MIN % -1`) and abort this debug build. +#[test] +fn test_constant_folding_int_min_mod_minus_one_does_not_panic() { + let out = compile_and_run(" $x, $a, $b);"#, + "array_map() with multiple arrays currently requires every array to share one scalar element type", + ); +} + +/// Verifies the two-input-array `array_map` form loudly rejects unsupported (float) element arrays, +/// since only integer and string element runtimes exist for the two-array path. +#[test] +fn test_error_array_map_two_arrays_unsupported_element_type() { + expect_error( + r#" $x + $y, $a, $b);"#, + "array_map() with multiple arrays currently supports only integer or string arrays", ); } @@ -140,7 +170,7 @@ fn test_error_array_pop_wrong_args() { /// Verifies that error in array wrong args. #[test] fn test_error_in_array_wrong_args() { - expect_error("