From a39d96149d178420b78b31260f70795d96f9051e Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 22:35:24 +0700
Subject: [PATCH] slice-from-ptr is refused with the slice-from call to write,
and the pointer cast's const rule is documented as covering the outer pointer
only
---
lib/check.ml | 21 +++++++++++++++++++--
test/test_flan.ml | 5 +++++
web/index.html | 2 +-
3 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/lib/check.ml b/lib/check.ml
index 18f4ce86..33633b18 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -9285,8 +9285,12 @@ and check_call ctx ~want loc (head : Ast.expr) (args : Ast.expr list) =
else, and checks nothing: the caller is promising the bytes are that
type, as with [slice-from]. Both backends already lower a Ptr-to-Ptr
[Cast] to no instruction. Adding const is allowed; dropping it is not,
- or a cast would undo the const [addr] put there. There is no cast between
- a pointer and an integer, and no pointer arithmetic. *)
+ or a cast would undo the const [addr] put there. The rule is about the
+ outer pointer only: the cast is unchecked, so ((Ptr (Ptr u8)) q) over a
+ (Ptr const (Ptr const u8)) is refused for the outer const but a
+ (Ptr (Ptr const u8)) casts to (Ptr (Ptr u8)) — what lies deeper is the
+ writer's promise, like everything else the cast asserts. There is no cast
+ between a pointer and an integer, and no pointer arithmetic. *)
| Ast.Call ({ Ast.e = Ast.Var "Ptr"; _ }, _)
when (match type_of_expr head with Some _ -> true | None -> false) ->
let target = resolve ctx.env (Option.get (type_of_expr head)) in
@@ -12688,6 +12692,19 @@ and ordinary_call ctx ~want loc name args =
says what the view is: over a Vec it borrows the storage the Vec \
owns, over an array or a string it looks at the value itself. \
Write %s" call
+ else if name = "slice-from-ptr" then
+ (* The name this form had before; code written against it lands here.
+ Said as the form to write, with the reader's arguments, and not as
+ a rename — a first-time reader has no old name to be told about. *)
+ let call =
+ match args with
+ | [ p; n ] ->
+ "(slice-from " ^ spell_arg "p" p ^ " " ^ spell_arg "n" n ^ ")"
+ | _ -> "(slice-from p n)"
+ in
+ Loc.failk "check/unknown-function" loc
+ "there is no slice-from-ptr. A slice from a pointer and a count of \
+ the elements behind it is slice-from. Write %s" call
else if no_such_rand name <> None then
(* A retired randomness name, which is a name and not a near miss:
"did you mean rand?" for [rand-f32] would be true and would not say
diff --git a/test/test_flan.ml b/test/test_flan.ml
index 861451e7..c92e97c5 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -2541,6 +2541,11 @@ let () =
rejects_check "a pointer cast of an integer"
"(defn f [x i64] (Ptr i32) ((Ptr i32) x))"
~needle:"no conversion between an integer and a pointer";
+ rejects_check "slice-from-ptr names slice-from"
+ "(defn f [p (Ptr i32) n i32] i32 (length (slice-from-ptr p n)))"
+ ~needle:"Write (slice-from p n)";
+ accepts "a pointer cast checks the outer const only"
+ "(defn f [q (Ptr (Ptr const u8))] (Ptr (Ptr u8)) ((Ptr (Ptr u8)) q))";
rejects_check "a pointer cast of a dyn"
"(defn f [x dyn] (Ptr i32) ((Ptr i32) x))"
~needle:"A dyn value never holds a pointer";
diff --git a/web/index.html b/web/index.html
index 44150fe4..570f9fcd 100644
--- a/web/index.html
+++ b/web/index.html
@@ -525,7 +525,7 @@ notation reads as exactly one data item.
(Map K V) | open addressing, owning, copies the same way. The only map spelling: braces in type position are not a type | data + len + log2cap + its allocator |
(Pool T) | generational slab storage, owning, copies the same way | items + slots + its allocator |
(Handle T) | a reference into a pool that reports a dead referent | index and generation packed into an i64 |
-(Ptr T) | raw pointer. ((Ptr U) p) reads it as a pointer to U; (slice-from p n) makes a [T] of the n elements behind it. Neither checks the memory | a pointer |
+(Ptr T) | raw pointer. ((Ptr U) p) reads it as a pointer to U, and may add const but not drop it (the outer pointer only: what it points at is taken on trust); (slice-from p n) makes a [T] of the n elements behind it. Neither checks the memory | a pointer |
(Ptr const T) | a pointer nothing is written through: the address of read-only storage, and what a C const T * takes. A (Ptr T) converts to one, never the reverse | a pointer |
(Option T) | Some / None | tag byte + T |
(Fn [T ...] R) | a function value, which may have captured | a code address and an environment pointer |