diff --git a/TODO.org b/TODO.org
index 081c65c5..0d77b3b9 100644
--- a/TODO.org
+++ b/TODO.org
@@ -944,20 +944,9 @@ blocker it was, since =(array 4 T)= answers the case that raised it. plan.org's
rule is "annotate function signatures, infer locals", so a general annotation is a
deliberate absence.
-** NEXT A read-only slice type
-Decided 2026-09-25: =[const u8]=, Zig's spelling in Flan's brackets. =bytes-view= answers one and a =set= through it is a compile error; a =[T]= converts to =[const T]= and not back, and the prelude's read-only functions take it. =const= is reserved as a name, since =[n T]= accepts a constant's name for =n=.
-=bytes-view= is read-only by convention only — the type system cannot say a =[u8]=
-may not be stored through, so a trap on read-only memory is the enforcement. A
-read-only slice type, or provenance, is what would move that refusal to compile
-time.
-
-** NEXT Writing through a string literal
-Decided 2026-09-25: closed by the read-only slice type above.
-=(let [s (bytes-view "Hi")] (set (at s 0) \h))= stores into read-only memory at
-=-O0= and is deleted as undefined at =-O2= — same source, and which way it fails
-depends on a flag. Narrowed when =(bytes s)= started copying, so the common
-spelling no longer reaches the edge. Emitting literals as mutable globals is not a
-fix: it moves which flag misbehaves and costs their read-only placement.
+** DONE A read-only slice type
+CLOSED: [2026-09-25]
+=[const T]=; a =[T]= converts at the top of a type or under another const slice, never inside a writable one. =(addr (at v i))= of a read-only element is allowed, since a =(Ptr T)= is the C boundary and has no const form; a store is what is refused.
** TODO (slice d 1) over a dyn string is refused where (at d i) works
The typed and dyn spaces disagree about a spelling, which the standing rule
diff --git a/calc-me.flan b/calc-me.flan
index 0af5019c..78d66745 100644
--- a/calc-me.flan
+++ b/calc-me.flan
@@ -20,7 +20,7 @@
;; ── one by pointer. `addr` takes the address of a local; the pointer never
;; ── outlives the frame, so no allocator is involved.
(defstruct Cursor
- [src [u8] ; non-owning slice into argv — calc-me never owns a byte
+ [src [const u8] ; non-owning slice into argv — calc-me never owns a byte
pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8
@@ -105,7 +105,7 @@
(Some lhs)))
;; ── Whole input, or nothing. Trailing junk is an error, not ignored. ──
-(defn evaluate [src [u8]] (Option f64)
+(defn evaluate [src [const u8]] (Option f64)
(let [c (Cursor {.src src})] ; pos omitted: zeroed
(let [v (some (parse-expr (addr c) 1))]
(skip-spaces (addr c))
diff --git a/docs/BUILT.md b/docs/BUILT.md
index 02803ee5..35e3289f 100644
--- a/docs/BUILT.md
+++ b/docs/BUILT.md
@@ -950,7 +950,7 @@ the only two under which a mark and a sweep run at all. `dev_segv` sits beside t
program that faults cannot be compared against an unsanitized run — that build's handler parks in the break loop, and
the two builds are *supposed* to differ, since `flan_dev_crash_enable` checks a weak `__asan_init` and declines to
install the handler when ASan is in the process. So the case asserts ASan's report and the absence of the handler's
-line, built at `-O0` because at `-O2` the write through a bytes-view of a literal does not fault at all. That yield had
+line, built at `-O0` because at `-O2` the write through a pointer to a literal's bytes does not fault at all. That yield had
never run in any build anywhere: it was behind a link that did not happen. Twenty-six seconds of the alias's 2m30 warm.
What it still does not reach is a program driven by a real daemon under ASan: `flan dev` builds its host through its own
path and has no `--sanitize` to pass it.
@@ -2794,7 +2794,7 @@ fires.
| `(clone v)` / `(clone v a)` | the only copy; assignment moves |
| `(free v)` | consumes its argument |
| `(bytes s)` / `(bytes s a)` | a writable copy of a string's bytes, against the context or a named allocator — an allocating operation like `vec-new`: StorageExhausted with retry, a registry note in dev builds. The answer is a `[u8]` view of the block, so nothing can `free` it through the slice; it lives until its allocator's `free-all` or destroy |
-| `(bytes-view s)` | the string's own storage as a `[u8]`, costing nothing — the old `(bytes s)` reinterpret, renamed. Read-only by convention: a literal's view points into `.rodata` and a store through it traps |
+| `(bytes-view s)` | the string's own storage as a `[const u8]`, costing nothing — the old `(bytes s)` reinterpret, renamed. A store through it is a compile error, because a literal's view points into `.rodata` |
### A view of a `Vec` goes stale at the `push`, and nothing checks it
@@ -3881,7 +3881,7 @@ held at once; these copy out of that buffer before returning, so the hazard ends
many numbers as it likes. `strings.flan` puts two integers and a float on one line, which is the case that could not
be written before.
-### `split` answers a `(Vec [u8])`, and the owning shape is unrepresentable
+### `split` answers a `(Vec [const u8])`, and the owning shape is unrepresentable
The fields are slices *of the input*. That was not a performance choice when this was written: `(Vec (Vec u8))` was
**refused outright**, so there was no owning shape to have chosen instead. That refusal has since been narrowed — see
@@ -3895,7 +3895,7 @@ The rule is `split-on-byte`'s, unchanged: n separators always yield n+1 fields,
field and a trailing separator yields a trailing empty one. That is Odin's allocating `strings.split` and not Odin's
`split_by_byte_iterator`, which disagree with each other on exactly that input.
-Constructing it needed a one-line `(defn slices-new [] (Vec [u8]) (vec-new))`, because `check.ml`'s `vec_new_elem`
+Constructing it needed a one-line `(defn slices-new [] (Vec [const u8]) (vec-new))`, because `check.ml`'s `vec_new_elem`
takes the element type as a single bare symbol and `[u8]` is not one — so a `(Vec [u8])` can only be made where the
*context* names the type, and a return type is a context while a `let` is not. Written down in TODO.org,
"(vec-new [u8]) is refused", as a compiler gap rather than worked around silently.
@@ -4538,7 +4538,7 @@ and the rule is easier to state and to trust with one construct in it.
## Assets are baked in, and the reason it is a compiler feature
-TODO.org, "Assets are embedded at compile time". `(embed "brush.png")` is a `[u8]`, `(embed "brush.png" string)` is a `string`, and
+TODO.org, "Assets are embedded at compile time". `(embed "brush.png")` is a `[const u8]`, `(embed "brush.png" string)` is a `string`, and
`(embed-dir "assets")` is a `[n EmbedFile]` sorted by name. Odin's `#load` and `#load_directory` are the model
(`src/parser.cpp`, and `check_load_directive` / `check_load_directory_directive` in `src/check_builtin.cpp`); Odin's
`#` is not imported, because an s-expression language already has a head position for a name and these resolve as
@@ -4551,12 +4551,12 @@ so a program can never be a package: the single file doing `(rl/load-texture "br
with **no link channel at all**. The web lane found that hole and did not invent a flag for it. Embedding has no such
hole, because there is nothing to tell the linker.
-**It costs nothing at run time.** The bytes reach the program as a `Tast.Str` node typed `[u8]`, which emit.ml turns
+**It costs nothing at run time.** The bytes reach the program as a `Tast.Str` node typed `[const u8]`, which emit.ml turns
into the same `private unnamed_addr constant` every string literal already becomes, and its `escape` is byte-exact
across the whole 0–255 range, so a PNG survives the round trip through the `.ll`. Bound with `defconst` at top level an
`embed-dir` is an LLVM constant outright, through emit.ml's `const`.
-**A `Str` node typed `[u8]`, not a `Bytes` prim over a `string`.** This is the one non-obvious choice. `Bytes` is
+**A `Str` node typed `[const u8]`, not a `Bytes` prim over a `string`.** This is the one non-obvious choice. `Bytes` is
identity — emit.ml lowers `Types.String` and `Types.Slice _` to the same `%slice` — but wrapping the literal in a prim
makes the node non-constant, and `const` then refuses an `embed-dir` in a `defconst` with *a global's value must be a
compile-time constant*. Both of emit.ml's string emitters take the bytes and ignore the node's type, so it is the same
@@ -4583,11 +4583,9 @@ the call reads `(embed-find (slice assets 0 (length assets)) "brush.png")`. Entr
order is filesystem-dependent and an unsorted embed would make two builds of identical sources emit different `.ll`.
Non-recursive, files only — Odin again.
-**The sharp edge, inherited and not widened.** The slice points into `.rodata`, so a store through it segfaults at
-`-O0` and is deleted as undefined behaviour at `-O2` — the same trap the prelude's ASCII-case note measures for
-`(bytes "Hi")`, and the same one TODO.org tracks as "Writing through a string literal". Nothing here makes it worse and
-nothing here fixes it; provenance is what would. **To get a mutable copy, clone the bytes into a `Vec`.** It is worth
-saying loudly because an embedded asset is precisely the thing someone will try to decode in place.
+**Read-only, and the type says so.** The slice points into `.rodata`, where a store would segfault at `-O0` and be
+deleted as undefined behaviour at `-O2`, so it is a `[const u8]` and a store through it is refused at compile time.
+To decode an asset in place, copy the bytes into a `Vec` first.
**What this does not do.** `sand.flan` still calls `(rl/load-texture "brush.png")`, which hands raylib a path for
raylib to open. Pointing raylib at embedded bytes needs `LoadImageFromMemory` and `LoadTextureFromImage` in place of
diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el
index f5a45fac..8edc20c6 100644
--- a/emacs/flan-mode.el
+++ b/emacs/flan-mode.el
@@ -238,7 +238,9 @@ reason and is the odd one — it is legal only as the last item of a `def' or a
;; resolves and the two function types, `Fn' and `CFn'. `dyn' is
;; lowercase on purpose — it is a primitive beside `i64' and `bool', not
;; a container over something.
- ;; `int' and `float' are builtin aliases for `i32' and `f32'.
+ ;; `int' and `float' are builtin aliases for `i32' and `f32'. `const'
+ ;; is the reserved word of the read-only slice type, `[const u8]', and is
+ ;; drawn as part of the type it spells.
;;
;; `Unit' is deliberately absent, though `Types.primitive_names' has it.
;; The resolver answers to the name because `Cimport' builds one for C's
@@ -246,7 +248,7 @@ reason and is the odd one — it is legal only as the last item of a `def' or a
;; word outright — unit is spelled `()'. Drawing it as a valid type would
;; advertise a spelling the parser rejects, which is the same reason
;; `find-restart' and `await' are left out of `flan--special'.
- ("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|dyn\\|int\\|float\\|Never\\|Allocator\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>"
+ ("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|dyn\\|const\\|int\\|float\\|Never\\|Allocator\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>"
. font-lock-type-face)
;; A type variable, `$t', which is what a generic `defn' names its
;; parameter types with and what `{:where (ordered? $t)}' constrains.
diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el
index be9d1761..0640f967 100644
--- a/emacs/test-flan-mode.el
+++ b/emacs/test-flan-mode.el
@@ -489,6 +489,8 @@
"a package alias")
("(defn f [x int] float 1.0)" "int" font-lock-type-face
"int, the builtin alias")
+ ("(defn f [s [const u8]] 1)" "const" font-lock-type-face
+ "const, in a read-only slice type")
;; Constants that stand for themselves.
("(set done true)" "true" font-lock-constant-face "true")
("(= o None)" "None" font-lock-constant-face "None")
diff --git a/examples/text-rectangle-bounds.flan b/examples/text-rectangle-bounds.flan
index fc4f7e81..8061a228 100644
--- a/examples/text-rectangle-bounds.flan
+++ b/examples/text-rectangle-bounds.flan
@@ -63,7 +63,7 @@
;; from here they are ordinary slices, bounds-checked like any other, and the
;; index comes from raylib's own get-glyph-index so it is in range by
;; construction.
-(defn draw-text-boxed [font rl/Font text [u8] rec rl/Rectangle
+(defn draw-text-boxed [font rl/Font text [const u8] rec rl/Rectangle
font-size f32 spacing f32 word-wrap? bool
tint rl/Color] ()
(let [glyphs (rl/font-glyphs font)
diff --git a/lib/ast.ml b/lib/ast.ml
index 41708a09..c507c66f 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -14,7 +14,7 @@ type texpr = { t : texpr_kind; tloc : Loc.t }
and texpr_kind =
| Tname of string (* i32 bool Cursor string *)
- | Tslice of texpr (* [u8] ptr+len *)
+ | Tslice of bool * texpr (* [u8] [const u8] ptr+len *)
| Tarray of len * texpr (* [4 f32] [rows [cols u32]] *)
| Tmap of texpr * texpr (* (Map string i32) *)
| Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *)
diff --git a/lib/check.ml b/lib/check.ml
index e5106991..652612ad 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -1137,7 +1137,8 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
let loc = t.Ast.tloc in
match t.Ast.t with
| Ast.Tname n -> resolve_name env ~seen loc n
- | Ast.Tslice e -> Types.Slice (resolve env ~seen e)
+ | Ast.Tslice (c, e) ->
+ Types.Slice ((if c then Types.Const else Types.Mut), resolve env ~seen e)
| Ast.Tarray (l, e) ->
let e = resolve env ~seen e in
no_zeroed_fn loc "a fixed array's element" e;
@@ -1660,7 +1661,7 @@ let rec bracket_value_element env values (t : Ast.texpr) =
| _ -> bracket_value_element env values e
in
match t.Ast.t with
- | Ast.Tslice e -> elem e
+ | Ast.Tslice (_, e) -> elem e
| Ast.Tarray (_, e) -> elem e
| _ -> None
@@ -1734,7 +1735,7 @@ let signature_tyvars (fn : Ast.fn) =
let rec ty (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> name t.Ast.tloc n
- | Ast.Tslice e -> ty e
+ | Ast.Tslice (_, e) -> ty e
| Ast.Tarray (_, e) -> ty e
| Ast.Tmap (k, v) -> ty k; ty v
(* The head of an application is a constructor — [Ptr], [Option], [Vec] —
@@ -1752,24 +1753,30 @@ let signature_tyvars (fn : Ast.fn) =
and with the same rule: a variable already bound must match what it is
bound to, so [(pair 1 2.0)] over [a $t b $t] is a refusal and not a
second instantiation. *)
-let rec bind_ty ?(widen = false) subst (pat : Types.t) (arg : Types.t) =
+(* [ro] is whether a [[T]] argument may meet a [[const $t]] pattern here: at
+ the top of an argument's type, and under a const slice, which is exactly
+ where [Types.const_widens] lets [expect] convert the value afterwards. *)
+let rec bind_ty ?(widen = false) ?(ro = true) subst (pat : Types.t)
+ (arg : Types.t) =
+ let inner = bind_ty ~ro:false subst in
match pat, arg with
| Types.Var v, a ->
(match List.assoc_opt v !subst with
| None -> subst := (v, a) :: !subst; true
| Some b -> Types.equal a b)
- | Types.Slice p, Types.Slice a
+ | Types.Slice (m, p), Types.Slice (m', a)
+ when m = m' || (ro && m = Types.Const) ->
+ bind_ty ~ro:(m = Types.Const) subst p a
| Types.Ptr p, Types.Ptr a
| Types.Vec p, Types.Vec a
- | Types.Option p, Types.Option a -> bind_ty subst p a
- | Types.Array (n, p), Types.Array (m, a) -> Int64.equal n m && bind_ty subst p a
- | Types.Map (k, v), Types.Map (k', v') ->
- bind_ty subst k k' && bind_ty subst v v'
+ | Types.Option p, Types.Option a -> inner p a
+ | Types.Array (n, p), Types.Array (m, a) -> Int64.equal n m && inner p a
+ | Types.Map (k, v), Types.Map (k', v') -> inner k k' && inner v v'
(* Each function type against its own. *)
| Types.Fn (ps, r), Types.Fn (ps', r')
| Types.CFn (ps, r), Types.CFn (ps', r') ->
List.length ps = List.length ps'
- && List.for_all2 (bind_ty subst) ps ps' && bind_ty subst r r'
+ && List.for_all2 inner ps ps' && inner r r'
(* And the widening between them, which is admitted at the top of an
argument's type and nowhere inside it.
[(Fn [$t] $t)] against a [(CFn [i32] i32)] is the shape every caller of
@@ -1796,7 +1803,7 @@ let rec bind_ty ?(widen = false) subst (pat : Types.t) (arg : Types.t) =
argument. *)
| Types.Fn (ps, r), Types.CFn (ps', r') when widen ->
List.length ps = List.length ps'
- && List.for_all2 (bind_ty subst) ps ps' && bind_ty subst r r'
+ && List.for_all2 inner ps ps' && inner r r'
(* Nothing generic left on the pattern side: this is ordinary type
equality, and [Never] fits anywhere exactly as it does elsewhere. *)
| p, a -> Types.fits ~expected:p ~actual:a
@@ -1804,7 +1811,7 @@ let rec bind_ty ?(widen = false) subst (pat : Types.t) (arg : Types.t) =
let rec subst_ty subst (t : Types.t) =
match t with
| Types.Var v -> (match List.assoc_opt v subst with Some c -> c | None -> t)
- | Types.Slice e -> Types.Slice (subst_ty subst e)
+ | Types.Slice (m, e) -> Types.Slice (m, subst_ty subst e)
| Types.Array (n, e) -> Types.Array (n, subst_ty subst e)
| Types.Map (k, v) -> Types.Map (subst_ty subst k, subst_ty subst v)
| Types.Ptr e -> Types.Ptr (subst_ty subst e)
@@ -1819,7 +1826,7 @@ let rec subst_ty subst (t : Types.t) =
let rec generic_ty (t : Types.t) =
match t with
| Types.Var _ -> true
- | Types.Slice e | Types.Array (_, e) | Types.Ptr e | Types.Vec e
+ | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr e | Types.Vec e
| Types.Option e -> generic_ty e
| Types.Map (k, v) -> generic_ty k || generic_ty v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
@@ -1833,7 +1840,7 @@ let rec generic_ty (t : Types.t) =
let rec reaches_dyn (t : Types.t) =
match t with
| Types.Dyn -> true
- | Types.Slice e | Types.Array (_, e) | Types.Ptr e | Types.Vec e
+ | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr e | Types.Vec e
| Types.Option e -> reaches_dyn e
| Types.Map (k, v) -> reaches_dyn k || reaches_dyn v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
@@ -1873,7 +1880,8 @@ let unconstrained env loc op ~needs (t : Types.t) =
let rec mangle_ty (t : Types.t) =
match t with
| Types.Unit -> "unit"
- | Types.Slice e -> "slice-" ^ mangle_ty e
+ | Types.Slice (Types.Mut, e) -> "slice-" ^ mangle_ty e
+ | Types.Slice (Types.Const, e) -> "cslice-" ^ mangle_ty e
| Types.Array (n, e) -> Printf.sprintf "arr%Ld-%s" n (mangle_ty e)
| Types.Map (k, v) -> Printf.sprintf "map-%s-%s" (mangle_ty k) (mangle_ty v)
| Types.Ptr e -> "ptr-" ^ mangle_ty e
@@ -1917,7 +1925,7 @@ let rec occurs_in ~needle (t : Types.t) =
Types.equal needle t
||
match t with
- | Types.Slice e | Types.Array (_, e) | Types.Ptr e | Types.Vec e
+ | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr e | Types.Vec e
| Types.Option e -> occurs_in ~needle e
| Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
@@ -2229,7 +2237,7 @@ let num_bytes = 64L
let to_bytes ctx loc pr (x : Tast.expr) =
let bty = Types.Array (num_bytes, Types.Int Types.U8) in
- let bslice = Types.Slice (Types.Int Types.U8) in
+ let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let s = fresh_slot ctx bty in
mk loc bslice
(Tast.Let
@@ -2551,7 +2559,16 @@ let box loc (e : Tast.expr) : Tast.expr =
| Some k ->
if not (permanent_root e) then view_not_permanent loc e.Tast.ty
else dyn "flan_dyn_view_vec" [ e; view_elem_lit loc k ])
- | Types.Slice elem ->
+ (* A dyn view is written through by (set (at d i) x), and nothing on the
+ dyn side can tell a read-only one apart, so a [[const T]] does not
+ cross. *)
+ | Types.Slice (Types.Const, elem) ->
+ no_dyn_yet loc ~into:true e.Tast.ty
+ (Printf.sprintf
+ ": a dyn view can be written through, and a [const %s] can only be \
+ read. A dyn view is taken of the writable storage it came from"
+ (Types.to_string elem))
+ | Types.Slice (Types.Mut, elem) ->
(match view_elem elem with
| None -> view_not_yet loc e.Tast.ty elem
| Some k ->
@@ -2849,7 +2866,8 @@ let rec thick_enc (t : Types.t) =
| Types.Named n -> "n" ^ atom n
| Types.Enum n -> "e" ^ atom n
| Types.Var v -> "y" ^ atom v
- | Types.Slice e -> "s" ^ thick_enc e
+ | Types.Slice (Types.Mut, e) -> "s" ^ thick_enc e
+ | Types.Slice (Types.Const, e) -> "k" ^ thick_enc e
| Types.Ptr e -> "p" ^ thick_enc e
| Types.Vec e -> "v" ^ thick_enc e
| Types.Option e -> "o" ^ thick_enc e
@@ -2911,6 +2929,27 @@ let numeric_note ~(want : Types.t) ~(got : Types.t) =
(%s x)"
(Types.to_string want)
+(* The rest of the sentence when a read-only slice meets a writable one. Both
+ copies it names compile today: [string] reads any byte slice and [bytes]
+ copies a string, and [into] pushes any slice's elements into a Vec that
+ [slice] then views. *)
+let const_note ~(want : Types.t) ~(got : Types.t) =
+ match want, got with
+ | Types.Slice (Types.Mut, e), Types.Slice (Types.Const, e')
+ when Types.equal e e' ->
+ let copy =
+ match e with
+ | Types.Int Types.U8 -> "(bytes (string v))"
+ | _ -> Printf.sprintf "(slice (into v (vec-new %s)))" (Types.to_string e)
+ in
+ Printf.sprintf
+ " — a %s can only be read, and never becomes a %s that can be written \
+ through. %s copies v into a %s of its own; where nothing writes \
+ through it, the %s can be declared %s instead"
+ (Types.to_string got) (Types.to_string want) copy (Types.to_string want)
+ (Types.to_string want) (Types.to_string got)
+ | _ -> ""
+
let expect ctx loc ~want (got : Tast.expr) =
match want with
| None -> got
@@ -2958,6 +2997,12 @@ let expect ctx loc ~want (got : Tast.expr) =
| Types.Fn (ps, r), Types.CFn (ps', r')
when Types.equal (Types.Fn (ps, r)) (Types.Fn (ps', r')) ->
mk loc w (Tast.Thicken (thick_thunk ctx.env loc ps r, got))
+ (* A writable view seen as a read-only one. The two are the same two
+ words, so the value is only retyped; the reverse is refused below,
+ with [const_note] naming the copy that would make it writable. *)
+ | Types.Slice (Types.Const, _), _
+ when Types.const_widens ~from:got.Tast.ty ~into:w ->
+ { got with Tast.ty = w }
| _ -> got
in
if Types.fits ~expected:w ~actual:got.Tast.ty then got
@@ -2971,9 +3016,10 @@ let expect ctx loc ~want (got : Tast.expr) =
numbers, and it is on this message rather than beside it because a
reader who has just been told i64 and i32 are different types needs
to be told, in the same breath, which direction needed nothing. *)
- Loc.failk "check/type-mismatch" loc "expected %s, found %s%s"
+ Loc.failk "check/type-mismatch" loc "expected %s, found %s%s%s"
(Types.to_string w) (Types.to_string got.Tast.ty)
(numeric_note ~want:w ~got:got.Tast.ty)
+ (const_note ~want:w ~got:got.Tast.ty)
(* Something a [break] may not jump out of, named so the refusal can say which.
See [lentry]: it is a barrier and not a blanket refusal, so a loop written
@@ -5474,7 +5520,7 @@ and check_arr ctx ~want loc items =
let elem_want =
match want with
| Some (Types.Array (_, t)) -> Some t
- | Some (Types.Slice t) -> Some t
+ | Some (Types.Slice (_, t)) -> Some t
| _ -> None
in
(* With nothing outside saying what the elements are, the first one says:
@@ -6117,7 +6163,44 @@ and refuse_string_place loc (ty : Types.t) =
"a string is read-only, so (at s i) is a value and not a place. Copy \
the bytes into a buffer you own and write that"
-and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
+(* The read-only slice a value's storage is reached through, if there is one:
+ an element of a [[const T]], a field of such an element, or an element of
+ an array that is. The last slice stepped through decides, because the
+ const is shallow — an element of a [[const [u8]]] is itself a writable
+ [[u8]], and what it views is not the outer slice's to protect. *)
+and const_reached (e : Tast.expr) =
+ match e.Tast.e with
+ | Tast.Prim (Tast.At, target :: idx) ->
+ const_steps (const_reached target) target.Tast.ty (List.length idx)
+ | Tast.Field (target, _) -> const_reached target
+ | _ -> None
+
+(* [ro] after stepping [n] dimensions into [ty], the way [indexed] steps. *)
+and const_steps ro (ty : Types.t) n =
+ if n = 0 then ro
+ else
+ match ty with
+ | Types.Slice (Types.Const, t) -> const_steps (Some ty) t (n - 1)
+ | Types.Slice (Types.Mut, t) -> const_steps None t (n - 1)
+ | Types.Array (_, t) -> const_steps ro t (n - 1)
+ | _ -> ro
+
+(* A store, or an [addr], through a read-only view. *)
+and refuse_const_place loc (view : Types.t) =
+ let elem = match view with Types.Slice (_, t) -> t | t -> t in
+ Loc.failk "check/store-through-const" loc
+ "this writes through a %s, which can only be read, so the element is a \
+ value and not a place. Write into a slice that can be written: \
+ (slice (into v (vec-new %s))) copies v's elements into one"
+ (Types.to_string view) (Types.to_string elem)
+
+(* [store] is false for [addr] alone. A (Ptr T) is the C boundary, where the
+ program is already trusted — [slice-from-ptr] and [declare-c] take its word
+ — and a [[const u8]] handed to a C function that takes a [const T *] has no
+ other way across: [load-image-from-memory] over an [embed] is the case. So
+ the address of a read-only element may be taken, and it is a store that is
+ refused. *)
+and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t =
match p with
| Ast.Pvar name ->
(* Scope first, and the capture refusal only where scope did not settle
@@ -6167,7 +6250,9 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
| None ->
Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
"%s has no field %s" sname name
- | Some i -> Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty)
+ | Some i ->
+ if store then Option.iter (refuse_const_place loc) (const_reached target);
+ Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty)
| Ast.Pindex (target, idx) ->
let target = check ctx target in
(match target.Tast.ty with
@@ -6179,7 +6264,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
let p, ty = vec_at ctx loc target idx in
Tast.Pderef p, ty
| _ ->
- let idx, ty = indexed ~place:loc ctx target idx in
+ let idx, ty = indexed ~place:loc ~store ctx target idx in
Tast.Pindex (target, idx), ty)
| Ast.Pderef target ->
let target = check ctx target in
@@ -6239,13 +6324,18 @@ and index_expr ctx (e : Ast.expr) =
at *every* dimension rather than once about the target: [(at g 0 0)] over a
[[2 string]] reaches a string at the last step and nowhere before it, so a
question asked only of [g] would miss it. *)
-and indexed ?place ctx (target : Tast.expr) (idx : Ast.expr list) =
+and indexed ?place ?(store = true) ctx (target : Tast.expr) (idx : Ast.expr list) =
+ (match place with
+ | Some l when store ->
+ Option.iter (refuse_const_place l)
+ (const_steps (const_reached target) target.Tast.ty (List.length idx))
+ | _ -> ());
let rec go ty = function
| [] -> [], ty
| i :: rest ->
let elem =
match ty with
- | Types.Array (_, t) | Types.Slice t -> t
+ | Types.Array (_, t) | Types.Slice (_, t) -> t
(* A string indexes to its bytes, and only to read them. *)
| Types.String ->
Option.iter (fun l -> refuse_string_place l ty) place;
@@ -6370,7 +6460,7 @@ and not_numeric name what (a : Tast.expr) =
let text =
match a.Tast.ty with
| Types.String -> true
- | Types.Slice (Types.Int Types.U8) -> true
+ | Types.Slice (_, (Types.Int Types.U8)) -> true
| _ -> false
in
let where = a.Tast.loc in
@@ -6784,7 +6874,10 @@ and type_of_expr (e : Ast.expr) : Ast.texpr option =
in
match e.Ast.e with
| Ast.TypeArg t -> Some t
- | Ast.Arr [ x ] -> Option.map (fun t -> mk (Ast.Tslice t)) (inner x)
+ (* Before the [[n T]] arm below, which would read [const] as a length. *)
+ | Ast.Arr [ { Ast.e = Ast.Var "const"; _ }; x ] ->
+ Option.map (fun t -> mk (Ast.Tslice (true, t))) (inner x)
+ | Ast.Arr [ x ] -> Option.map (fun t -> mk (Ast.Tslice (false, t))) (inner x)
| Ast.Arr [ { Ast.e = Ast.Int n; _ }; x ] ->
Option.map (fun t -> mk (Ast.Tarray (Ast.Lint n, t))) (inner x)
| Ast.Arr [ { Ast.e = Ast.Var n; _ }; x ] ->
@@ -6940,17 +7033,17 @@ and vec_slice ctx ~want loc (target : Tast.expr) elem (bounds : Ast.expr list) =
lo, hi
| _ -> assert false
in
- let out = fresh_slot ctx (Types.Slice elem) in
+ let out = fresh_slot ctx (Types.Slice (Types.Mut, elem)) in
let fill =
rt loc Types.Unit "flan_vec_as_slice"
- [ target; addr_of loc (mk loc (Types.Slice elem) (Tast.Local out));
+ [ target; addr_of loc (mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out));
lo; hi; size_of loc elem; here loc ]
in
expect ctx loc ~want
- (mk loc (Types.Slice elem)
- (Tast.Let ([ (out, mk loc (Types.Slice elem)
- (Tast.Zero (Types.Slice elem))) ],
- [ fill; mk loc (Types.Slice elem) (Tast.Local out) ])))
+ (mk loc (Types.Slice (Types.Mut, elem))
+ (Tast.Let ([ (out, mk loc (Types.Slice (Types.Mut, elem))
+ (Tast.Zero (Types.Slice (Types.Mut, elem)))) ],
+ [ fill; mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out) ])))
(* Every arm below is a name an editor can be asked about and no program ever
wrote down, so each one needs a line in [builtins] further down this file.
@@ -7908,7 +8001,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| [ s ] ->
let s = check ctx s in
(match s.Tast.ty with
- | Types.String | Types.Slice (Types.Int Types.U8) ->
+ | Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_kw" [ s ])
| other ->
fail loc "keyword takes a string or a [u8], found %s"
@@ -8073,13 +8166,10 @@ and named_call ?(qualified = false) ctx ~want loc name args =
the round trip through the .ll. Bound with [defconst], an [embed-dir]
becomes an LLVM constant outright (emit.ml's [const]).
- The one sharp edge, and it is not new: the slice this hands back points
- into .rodata, so a store through it either segfaults at -O0 or is deleted
- at -O2 — the same measured trap the prelude's ASCII-case note describes
- for (bytes-view "Hi"). Copy the bytes — (bytes s) does exactly that for a
- string — for a mutable buffer. Nothing here widens that hole; it inherits
- it, and read-only slice types are what would close it (TODO.org, "A
- read-only slice type"). *)
+ The slice this hands back points into .rodata, so it is a [const u8]: a
+ store through it would segfault at -O0 and be deleted at -O2, and the
+ type refuses it at compile time instead. Copy the bytes for a writable
+ buffer. *)
| "embed" ->
(match args with
| [ p ] | [ p; _ ] ->
@@ -8091,17 +8181,17 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| [ _; { Ast.e = Ast.Var "string"; _ } ] | [ _ ] -> ()
| [ _; t ] ->
fail t.Ast.loc
- "embed's second argument is string, or nothing for a [u8]"
+ "embed's second argument is string, or nothing for a [const u8]"
| _ -> ());
let data = read_embed_file (embed_path loc p) p.Ast.loc in
let as_string () = mk loc Types.String (Tast.Str data) in
- (* A [Str] node typed [u8] rather than a [Bytes] prim over one. [Bytes]
+ (* A [Str] node typed [const u8] rather than a [Bytes] prim over one. [Bytes]
is identity — emit.ml lowers String and Slice _ to the same %slice —
and the prim would make the node non-constant, so an (embed-dir) in a
defconst could not be an LLVM constant. Both of emit.ml's string
emitters take the bytes and ignore the node's type, so this is the
same constant either way, and it is one a global can hold. *)
- let as_bytes () = mk loc (Types.Slice (Types.Int Types.U8)) (Tast.Str data) in
+ let as_bytes () = mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Str data) in
(* Two spellings rather than one that changes type with its context.
Odin threads a type_hint everywhere and can afford (embed "p") to
mean a string here and a []u8 there; with structural equality and a
@@ -8118,7 +8208,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| _ -> expect ctx loc ~want (as_bytes ())))
| _ ->
fail loc
- "embed is (embed \"path\") for a [u8], or (embed \"path\" string)")
+ "embed is (embed \"path\") for a [const u8], or (embed \"path\" string)")
(* ── What a macro says when it has to refuse ───────────────────
The one thing a macro could not do, written down in the prelude where
[unless] settles for it: "a macro has no error facility: it runs inside
@@ -8166,7 +8256,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
(Tast.Make
("EmbedFile",
[ mk loc Types.String (Tast.Str nm);
- mk loc (Types.Slice (Types.Int Types.U8)) (Tast.Str data) ])))
+ mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Str data) ])))
entries
in
expect ctx loc ~want
@@ -8246,11 +8336,11 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let path = check ctx ~want:Types.String path in
let data = byte_slice ctx data in
let ps = fresh_slot ctx Types.String in
- let ds = fresh_slot ctx (Types.Slice (Types.Int Types.U8)) in
+ let ds = fresh_slot ctx (Types.Slice (Types.Const, Types.Int Types.U8)) in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) "flan_file_write"
[ mk loc Types.String (Tast.Local ps);
- mk loc (Types.Slice (Types.Int Types.U8)) (Tast.Local ds) ]) ]
+ mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Local ds) ]) ]
in
(* Both operands are bound before the loop so that a retry re-attempts
the write and not the expressions that produced it — the same rule
@@ -8420,7 +8510,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
calling it a byte slice would hand out a writable-looking view of
storage the program does not own. *)
let result = match ty with
- | Types.Array (_, t) | Types.Slice t -> Types.Slice t
+ | Types.Array (_, t) -> Types.Slice (Types.Mut, t)
+ | Types.Slice (m, t) -> Types.Slice (m, t)
| Types.String -> Types.String
| other ->
fail loc
@@ -8545,7 +8636,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
fail n_loc
"slice-from-ptr length %Ld is negative" k
| _ -> ());
- prim Tast.SliceFromPtr (Types.Slice elem) [ target; n ]
+ prim Tast.SliceFromPtr (Types.Slice (Types.Mut, elem)) [ target; n ]
| _ -> assert false)
(* ── pointers ──────────────────────────────────────────────────── *)
@@ -8558,7 +8649,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
"addr takes the address of a place — a name, (.field x), (at a i) \
or (deref p)"
| Some p ->
- let p, ty = check_place ctx a.Ast.loc p in
+ let p, ty = check_place ~store:false ctx a.Ast.loc p in
expect ctx loc ~want (mk loc (Types.Ptr ty) (Tast.Addr p)))
| "deref" ->
arity ctx loc name 1 args;
@@ -8600,17 +8691,17 @@ and named_call ?(qualified = false) ctx ~want loc name args =
expect ctx loc ~want (mk loc (Types.Option a.Tast.ty) (Tast.Some_ a))
(* ── the milestone-2 host primitives (plan.org) ────────────────── *)
- (* (bytes-view s): the string's own storage seen as a [u8], costing nothing.
- This is what (bytes s) used to be, renamed for what it is: a *view*. The
- slice aliases the string — a literal's view points into .rodata and a
- store through it traps at -O0 on either backend — so it is read-only by
- convention until the type system can say so (TODO.org, "A read-only
- slice type").
+ (* (bytes-view s): the string's own storage seen as a [const u8], costing
+ nothing. The slice aliases the string, and a literal's bytes are in
+ read-only memory — a store through them would trap at -O0 and be deleted
+ as undefined at -O2 — so the view is one that can only be read, and a
+ store through it is refused here rather than at run time. (bytes s) is
+ the writable copy.
Reading through it is the whole use: bytes=?, split, index-of-bytes and
every other comparison walks a string's bytes without copying them. *)
| "bytes-view" ->
arity ctx loc name 1 args;
- prim Tast.Bytes (Types.Slice (Types.Int Types.U8))
+ prim Tast.Bytes (Types.Slice (Types.Const, Types.Int Types.U8))
[ check ctx ~want:Types.String (List.hd args) ]
(* (bytes s) / (bytes s a): a *writable copy* of the string's bytes, from
@@ -8641,7 +8732,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
the string. Same rule as [push]'s element. *)
let sv = fresh_slot ctx Types.String in
let v = fresh_slot ctx (Types.Vec u8) in
- let out = fresh_slot ctx (Types.Slice u8) in
+ let out = fresh_slot ctx (Types.Slice (Types.Mut, u8)) in
let attempt =
rt loc (Types.Int Types.I8) "flan_bytes_dup"
[ mk loc (Types.Vec u8) (Tast.Local v); a;
@@ -8650,23 +8741,23 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let fill =
rt loc Types.Unit "flan_vec_as_slice"
[ mk loc (Types.Vec u8) (Tast.Local v);
- addr_of loc (mk loc (Types.Slice u8) (Tast.Local out));
+ addr_of loc (mk loc (Types.Slice (Types.Mut, u8)) (Tast.Local out));
mk loc index_ty (Tast.Int (0L, Types.I32));
mk loc index_ty (Tast.Int (-1L, Types.I32));
size_of loc u8; here loc ]
in
expect ctx loc ~want
- (mk loc (Types.Slice u8)
+ (mk loc (Types.Slice (Types.Mut, u8))
(Tast.Let
([ (sv, s);
(v, mk loc (Types.Vec u8) (Tast.Zero (Types.Vec u8)));
- (out, mk loc (Types.Slice u8) (Tast.Zero (Types.Slice u8))) ],
+ (out, mk loc (Types.Slice (Types.Mut, u8)) (Tast.Zero (Types.Slice (Types.Mut, u8)))) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec"
(mk loc (Types.Vec u8) (Tast.Local v))
[ size_of loc u8 ] u8);
fill;
- mk loc (Types.Slice u8) (Tast.Local out) ])))
+ mk loc (Types.Slice (Types.Mut, u8)) (Tast.Local out) ])))
| _ -> fail loc "bytes is (bytes s) or (bytes s allocator)")
(* (string b): a [u8] seen as a string. The mirror of (bytes-view s),
@@ -8695,12 +8786,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
would be the only enforcement point in the language — a claim the rest
of it does not make.
- 2. It does not widen the literal-write hole (TODO.org, "Writing through a
- string literal"). That hole is the other direction: (bytes-view "Hi")
- hands you a writable-looking slice over constant data — narrowed since
- (bytes s) became a copy, and closable only by read-only slice types
- (TODO.org, "A read-only slice type"). This direction only loses the
- ability to write — a string
+ 2. It takes a [const u8], so a [u8] and a (bytes-view s) are both
+ accepted. This direction only loses the ability to write — a string
is read-only everywhere — so the result of (string b) can reach
strictly fewer stores than b could.
@@ -8812,7 +8899,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
if List.exists (fun a -> generic_ty a.Tast.ty) checked then
mk loc Types.Unit Tast.Unit
else
- let bslice = Types.Slice (Types.Int Types.U8) in
+ let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let write x = mk loc Types.Unit (Tast.Prim (Tast.WriteStdout, [ x ])) in
(* One frame slot per conversion the printer emits, which is what
[to_bytes] is for. The printer writes each number out before making the
@@ -8836,7 +8923,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let rc = render_ctx ctx emitter in
let render_one a =
match a.Tast.ty with
- | Types.String | Types.Slice (Types.Int Types.U8) ->
+ | Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
[ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ]
| _ -> Render.render rc 0 a
in
@@ -8884,7 +8971,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
if generic_ty v.Tast.ty then mk loc Types.Unit Tast.Unit
else begin
let unit_rt sym args = mk loc Types.Unit (Tast.Prim (Tast.Rt sym, args)) in
- let bslice = Types.Slice (Types.Int Types.U8) in
+ let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let emitter : Render.emitter =
{ Render.ebytes = (fun x -> unit_rt "flan_dev_watch_emit" [ x ]);
estr = (fun x -> unit_rt "flan_dev_watch_emit_str" [ x ]);
@@ -8943,7 +9030,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
| "argv" ->
arity ctx loc name 0 args;
- prim Tast.Argv (Types.Slice Types.String) []
+ prim Tast.Argv (Types.Slice (Types.Mut, Types.String)) []
(* ── casts: (i32 x), (f64 x), and an enum both ways ────────────────
@@ -9428,7 +9515,7 @@ and generic_call ctx ~want loc name vars pats pret args =
let rec mentions v (t : Types.t) =
match t with
| Types.Var u -> String.equal u v
- | Types.Slice e | Types.Array (_, e) | Types.Ptr e | Types.Vec e
+ | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr e | Types.Vec e
| Types.Option e -> mentions v e
| Types.Map (k, w) -> mentions v k || mentions v w
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
@@ -9537,8 +9624,16 @@ and generic_call ctx ~want loc name vars pats pret args =
(* [~widen]: this is the top of an argument's type, which is the one
place a widening thunk can be built around it. See [bind_ty]. *)
if (not handled) && not (bind_ty ~widen:true subst p a.Tast.ty) then
- fail a.Tast.loc "%s expects %s here, found %s" name
- (Types.to_string p) (Types.to_string a.Tast.ty);
+ fail a.Tast.loc "%s expects %s here, found %s%s" name
+ (Types.to_string p) (Types.to_string a.Tast.ty)
+ (match p, a.Tast.ty with
+ | Types.Slice (Types.Mut, _), Types.Slice (Types.Const, e) ->
+ Printf.sprintf
+ " — %s takes a slice it may write through, and a %s can \
+ only be read. (slice (into v (vec-new %s))) copies v into \
+ one that can be written"
+ name (Types.to_string a.Tast.ty) (Types.to_string e)
+ | _ -> "");
a)
pats args
in
@@ -9792,7 +9887,7 @@ and is_cast name =
Types.ikind_of_name name <> None || Types.fkind_of_name name <> None
and byte_slice ctx (a : Ast.expr) =
- check ctx ~want:(Types.Slice (Types.Int Types.U8)) a
+ check ctx ~want:(Types.Slice (Types.Const, Types.Int Types.U8)) a
and numeric_want want =
match want with Some (Types.Int _ | Types.Float _) -> want | _ -> None
@@ -10211,13 +10306,13 @@ let builtins : (string * string * string) list =
It is what a defgeneric dispatches on, so a class dispatcher is this \
call over the first argument and a defmulti whose body is (class-of x) \
is the same generic function written the other way.");
- ("keyword", "keyword [string|[u8]] dyn",
+ ("keyword", "keyword [string|[const u8]] dyn",
"The interned dyn keyword named by the bytes, for a name that only \
exists at run time — a reader building :texture-path out of a token's \
text. A literal :foo is already one.");
(* assets, embedded at compile time *)
- ("embed", "embed [\"path\" string?] [u8]",
+ ("embed", "embed [\"path\" string?] [const u8]",
"The file's bytes, read at compile time and baked in as a constant; \
(embed \"p\" string) reads it as a string instead. The path is \
relative to the file the form is written in, and the slice points into \
@@ -10235,7 +10330,7 @@ let builtins : (string * string * string) list =
"Reads a whole file. No Result and no out-parameter: a failure to read \
signals FileError under retry and use-value, and a failure to allocate \
signals StorageExhausted.");
- ("barf", "barf [string [u8]] ()",
+ ("barf", "barf [string [const u8]] ()",
"Writes a whole file. On the web target it signals FileError every \
time, with the path — there is no conditional compilation, so the \
program decides rather than the build.");
@@ -10289,17 +10384,17 @@ let builtins : (string * string * string) list =
StorageExhausted with retry — and the block lives until its \
allocator's free-all or destroy. For reading without a copy, \
bytes-view.");
- ("bytes-view", "bytes-view [string] [u8]",
- "The string's own storage seen as a byte slice. It costs nothing — both \
- are a ptr and a length at run time — and it decodes nothing. \
- Read-only by convention: a literal's bytes are constant data, so the \
- slice looks writable and a store through it traps.");
- ("string", "string [[u8]] string",
+ ("bytes-view", "bytes-view [string] [const u8]",
+ "The string's own storage seen as a read-only byte slice. It costs \
+ nothing — both are a ptr and a length at run time — and it decodes \
+ nothing. A store through it is a compile error; bytes is the writable \
+ copy.");
+ ("string", "string [[const u8]] string",
"A byte slice seen as a string, and free at run time. It does not check \
UTF-8, because `string` does not claim UTF-8 — valid-utf8? is an \
ordinary function you call when you care.");
- ("bytes->f64", "bytes->f64 [[u8]] f64", "Parses a float out of the bytes.");
- ("bytes->i64", "bytes->i64 [[u8]] i64",
+ ("bytes->f64", "bytes->f64 [[const u8]] f64", "Parses a float out of the bytes.");
+ ("bytes->i64", "bytes->i64 [[const u8]] i64",
"Parses an integer out of the bytes.");
("f64->bytes", "f64->bytes [f64] [u8]",
"The number's text, in a frame slot belonging to this call site — so \
@@ -10308,7 +10403,7 @@ let builtins : (string * string * string) list =
("i64->bytes", "i64->bytes [i64] [u8]",
"The number's text, in a frame slot belonging to this call site; it \
does not survive the frame.");
- ("write-stdout", "write-stdout [[u8]] ()",
+ ("write-stdout", "write-stdout [[const u8]] ()",
"Writes the bytes to standard output exactly as given: no newline and \
no formatting.");
("print", "print [T ...] ()",
@@ -10479,6 +10574,15 @@ let collect env (decls : Ast.decl list) =
spelled with it reaches the compiler's builtins and never a \
declaration — nothing could call this one"
n builtin_prefix
+ (* [[const u8]] is a read-only slice only because no constant can be
+ named [const]: [[n T]] takes a constant's name for [n], and a
+ declaration of that name would make the brackets mean two things.
+ A local cannot be an array length, so only a declaration is
+ refused. *)
+ | Some "const" ->
+ Loc.failk "check/reserved-const" d.Ast.dloc
+ "const cannot be declared: it is reserved for the read-only slice \
+ type, [const T]. Choose another name"
| _ -> ())
decls;
let claimed = Hashtbl.create 64 in
@@ -11501,7 +11605,7 @@ let check_main env decls =
let ok_params =
match params with
| [] -> true
- | [ Types.Slice Types.String ] -> true
+ | [ Types.Slice (_, Types.String) ] -> true
| _ -> false
in
if not ok_params then
@@ -11750,7 +11854,7 @@ let rec dyn_reach ~through p seen (t : Types.t) =
| Types.Dyn -> true
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v
- | Types.Ptr e | Types.Slice e -> through && go e
+ | Types.Ptr e | Types.Slice (_, e) -> through && go e
| Types.Fn _ -> false
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
@@ -11805,7 +11909,7 @@ let rec dyn_behind_pointer p seen (t : Types.t) =
It still terminates. This walk's own [seen] guards its own [Named]
recursion, and each crossing starts a separate finite walk of its own. *)
- | Types.Ptr e | Types.Slice e -> dyn_through p [] e
+ | Types.Ptr e | Types.Slice (_, e) -> dyn_through p [] e
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v
| Types.Dyn | Types.Fn _ -> false
@@ -11880,7 +11984,7 @@ let rec hidden_dyn p seen (t : Types.t) : Types.t option =
if dyn_anywhere p seen k || dyn_anywhere p seen v then Some t else None
(* A pointer and a slice are views of storage something else roots; see the
note above. What they point at is checked where it is declared. *)
- | Types.Ptr e | Types.Slice e -> hidden_dyn p seen e
+ | Types.Ptr e | Types.Slice (_, e) -> hidden_dyn p seen e
| Types.Fn _ -> None
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
@@ -12029,7 +12133,7 @@ let dyn_descriptors (p : Tast.program) =
foreign parameter of pointer or slice type receives is the address
of a place. Below it the question is [dyn_behind_pointer]'s again. *)
let below (t : Types.t) =
- match t with Types.Ptr e | Types.Slice e -> e | t -> t
+ match t with Types.Ptr e | Types.Slice (_, e) -> e | t -> t
in
List.iteri
(fun i t ->
diff --git a/lib/cimport.ml b/lib/cimport.ml
index 0e4823c9..7e0d5871 100644
--- a/lib/cimport.ml
+++ b/lib/cimport.ml
@@ -408,7 +408,8 @@ let rec ty_source (t : Ast.texpr) =
| Ast.Tname n -> n
| Ast.Tapp (n, args) ->
Printf.sprintf "(%s %s)" n (String.concat " " (List.map ty_source args))
- | Ast.Tslice e -> Printf.sprintf "[%s]" (ty_source e)
+ | Ast.Tslice (c, e) ->
+ Printf.sprintf "[%s%s]" (if c then "const " else "") (ty_source e)
| Ast.Tarray (Ast.Lint n, e) -> Printf.sprintf "[%Ld %s]" n (ty_source e)
| Ast.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (ty_source e)
| Ast.Tmap (k, v) ->
diff --git a/lib/emit.ml b/lib/emit.ml
index 800e4a71..c2b3c8e2 100644
--- a/lib/emit.ml
+++ b/lib/emit.ml
@@ -765,7 +765,7 @@ let rec dty m d (t : Types.t) : int =
| Types.String ->
composite "string"
[ ("ptr", Types.Ptr (Types.Int Types.U8)); ("len", Types.Int Types.I64) ]
- | Types.Slice e ->
+ | Types.Slice (_, e) ->
composite (Types.to_string t)
[ ("ptr", Types.Ptr e); ("len", Types.Int Types.I64) ]
| Types.Option e ->
@@ -2488,7 +2488,7 @@ and element_addr f (target : Tast.expr) idx =
same way, bounds check included. *)
| Types.Slice _ | Types.String ->
let elem =
- match ty with Types.Slice e -> e | _ -> Types.Int Types.U8 in
+ match ty with Types.Slice (_, e) -> e | _ -> Types.Int Types.U8 in
(* A slice is ptr+len, so step through the pointer it holds. *)
let s = load f ptr ty in
let base = fresh f in
@@ -3328,7 +3328,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
p (ll target.Tast.ty) a lo64;
p
- | Types.Slice elem ->
+ | Types.Slice (_, elem) ->
let v = value f target in
let q = fresh f in
ins f "%s = extractvalue %%slice %s, 0" q v;
@@ -3434,9 +3434,9 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
term f "unreachable";
"zeroinitializer"
| Tast.Argv, [] ->
- let tmp = alloca f (Types.Slice Types.String) in
+ let tmp = alloca f (Types.Slice (Types.Mut, Types.String)) in
ins f "call void @flan_argv(ptr %s)" tmp;
- load f tmp (Types.Slice Types.String)
+ load f tmp (Types.Slice (Types.Mut, Types.String))
(* One arm for every runtime entry point the allocator and container runtime
has. The result type is the node's own and the argument types are the
arguments' own, so nothing here has to know which symbol it is calling. *)
@@ -3538,17 +3538,17 @@ and shim_in f name ret x =
and shim_out f name (x : Tast.expr) (buf : Tast.expr) =
let v = value f x in
let b = value f buf in
- let tmp = alloca f (Types.Slice (Types.Int Types.U8)) in
+ let tmp = alloca f (Types.Slice (Types.Mut, (Types.Int Types.U8))) in
ins f "call void %s(%s %s, ptr %s, ptr %s)" name (ll x.Tast.ty) v b tmp;
- load f tmp (Types.Slice (Types.Int Types.U8))
+ load f tmp (Types.Slice (Types.Mut, (Types.Int Types.U8)))
(* Slice in, slice out: [shim_in] returns a scalar and [shim_out] takes one, so
a shim that transforms bytes into bytes is neither. *)
and shim_in_out f name (x : Tast.expr) =
let p, n = explode f x in
- let tmp = alloca f (Types.Slice (Types.Int Types.U8)) in
+ let tmp = alloca f (Types.Slice (Types.Mut, (Types.Int Types.U8))) in
ins f "call void %s(ptr %s, i64 %s, ptr %s)" name p n tmp;
- load f tmp (Types.Slice (Types.Int Types.U8))
+ load f tmp (Types.Slice (Types.Mut, (Types.Int Types.U8)))
and cast f ~guard (x : Tast.expr) target =
let v = value f x in
diff --git a/lib/js.ml b/lib/js.ml
index c687f59b..221faf9e 100644
--- a/lib/js.ml
+++ b/lib/js.ml
@@ -219,7 +219,7 @@ let rec refuse_ty loc (t : Types.t) =
match t with
| Types.Int _ | Types.Float _ | Types.Bool | Types.String | Types.Unit
| Types.Never | Types.Named _ | Types.Enum _ -> ()
- | Types.Slice t | Types.Array (_, t) | Types.Option t -> refuse_ty loc t
+ | Types.Slice (_, t) | Types.Array (_, t) | Types.Option t -> refuse_ty loc t
| Types.Vec t -> refuse_ty loc t
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.iter (refuse_ty loc) ps; refuse_ty loc r
@@ -654,7 +654,7 @@ let struct_of m loc (t : Types.t) =
let elem_ty loc (t : Types.t) =
match t with
- | Types.Slice e | Types.Array (_, e) -> e
+ | Types.Slice (_, e) | Types.Array (_, e) -> e
| Types.String -> Types.Int Types.U8
| t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t)
diff --git a/lib/load.ml b/lib/load.ml
index a3f28a9b..beae5abd 100644
--- a/lib/load.ml
+++ b/lib/load.ml
@@ -198,7 +198,7 @@ let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr =
match t.Ast.t with
| Ast.Tname n when List.mem n owned -> Ast.Tname (qualify alias n)
| Ast.Tname _ as k -> k
- | Ast.Tslice e -> Ast.Tslice (rename_texpr owned alias e)
+ | Ast.Tslice (c, e) -> Ast.Tslice (c, rename_texpr owned alias e)
(* The length too: [rows] in [[rows [cols u32]]] is an ordinary
compile-time constant of the package, not part of the type syntax. *)
| Ast.Tarray (l, e) ->
@@ -742,7 +742,7 @@ let exported n = not (String.equal n "main")
let rec texpr_uses acc (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> acc := (n, t.Ast.tloc) :: !acc
- | Ast.Tslice e -> texpr_uses acc e
+ | Ast.Tslice (_, e) -> texpr_uses acc e
| Ast.Tarray (l, e) ->
(match l with Ast.Lname n -> acc := (n, t.Ast.tloc) :: !acc | Ast.Lint _ -> ());
texpr_uses acc e
diff --git a/lib/parse.ml b/lib/parse.ml
index c380830b..9002c5bf 100644
--- a/lib/parse.ml
+++ b/lib/parse.ml
@@ -89,10 +89,23 @@ let rec texpr (f : Form.t) : Ast.texpr =
emitter go on speaking. *)
| Sym "Unit" -> fail f "unit is written (), not Unit"
| Sym s -> mk (Ast.Tname s)
- | Vec [ elem ] -> mk (Ast.Tslice (texpr elem))
+ (* [const T] is matched before [n T], which it would otherwise be: [const]
+ is a reserved name exactly so that no constant can be called that and
+ make the two spellings mean the same brackets. *)
+ | Vec [ { v = Sym "const"; _ } ] ->
+ fail f "[const] names no element type — a read-only slice is [const T]"
+ | Vec [ { v = Sym "const"; _ }; elem ] -> mk (Ast.Tslice (true, texpr elem))
+ | Vec [ elem ] -> mk (Ast.Tslice (false, texpr elem))
| Vec [ n; elem ] -> mk (Ast.Tarray (len n, texpr elem))
+ | Vec items when List.exists (fun (i : Form.t) -> i.v = Sym "const") items ->
+ fail f
+ "a read-only slice is written [const T], and a fixed array [n T] has no \
+ read-only form — take a read-only view of one with (slice a) where a \
+ [const T] is wanted"
| Vec _ ->
- fail f "a type in brackets is [T] for a slice or [n T] for a fixed array"
+ fail f
+ "a type in brackets is [T] for a slice, [const T] for a read-only \
+ slice or [n T] for a fixed array"
(* Braces are not a type. [{K V}] used to spell [(Map K V)] and the two
resolved to the same thing; the brace spelling is withdrawn, and the
refusal names the surviving one rather than letting the form fall through
@@ -1805,7 +1818,7 @@ let rec decl (f : Form.t) : Ast.decl =
what keeps the compiler's own parameter out of the way of
every name the author might bind. Same trick as [gensym]. *)
params = [ { Ast.fname = macro_args;
- fty = { Ast.t = Ast.Tslice form_t; tloc = ps.loc };
+ fty = { Ast.t = Ast.Tslice (false, form_t); tloc = ps.loc };
floc = ps.loc } ];
(* Written out, not deferred: a macro takes [[Form]] and
returns a [Form], and neither half of that is the user's to
diff --git a/lib/prelude.ml b/lib/prelude.ml
index dee1d391..1b67f283 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -274,7 +274,7 @@ let source = {flan|
;; One family per element type, because there are no generics: each of these
;; is a *copy* per element type, and the set below is i32 (what indices, ids
;; and tile values are), f32 (what positions, velocities and weights are) and
-;; [u8] (what a field coming out of `split` is).
+;; [const u8] (what a field coming out of `split` is).
;;
;; A slice is ptr+len and non-owning, so these mutate the storage they were
;; handed: sorting (slice grid 4 9) sorts those five elements of grid and
@@ -389,7 +389,7 @@ let source = {flan|
;; The first index holding x. None rather than -1, because Option is what the
;; language has and a sentinel index is the bug this avoids.
-(defn index-of [s [$t] x $t] (Option i32)
+(defn index-of [s [const $t] x $t] (Option i32)
{:where (equal? $t)}
(dotimes [i (length s)]
(when (= (at s i) x)
@@ -406,7 +406,7 @@ let source = {flan|
;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+]
;; either. These reduce a slice, which is a different operation with a
;; different arity, so the different name is honest rather than a workaround.
-(defn min-of [s [$t]] (Option $t)
+(defn min-of [s [const $t]] (Option $t)
{:where (ordered? $t)}
(if (= (length s) 0)
None
@@ -415,7 +415,7 @@ let source = {flan|
(set m (min m (at s i))))
(Some m))))
-(defn max-of [s [$t]] (Option $t)
+(defn max-of [s [const $t]] (Option $t)
{:where (ordered? $t)}
(if (= (length s) 0)
None
@@ -500,7 +500,7 @@ let source = {flan|
;; The general fold, of which sum-i32 is the special case with the + written
;; in. The accumulator comes first in the step, which is the order that reads
;; as (f acc x) and the order Odin's slice.reduce uses.
-(defn reduce [s [$t] init $t f (Fn [$t $t] $t)] $t
+(defn reduce [s [const $t] init $t f (Fn [$t $t] $t)] $t
(let [acc init]
(dotimes [i (length s)]
(set acc (f acc (at s i))))
@@ -513,7 +513,7 @@ let source = {flan|
;; allocates — (vec-new t), push, returns (Vec t) — and the type-erased Vec
;; runtime needed no change at all, because SizeOf and AlignOf are computed at
;; the instantiation site, where the element type is concrete.
-(defn filter [s [$t] keep? (Fn [$t] bool)] (Vec $t)
+(defn filter [s [const $t] keep? (Fn [$t] bool)] (Vec $t)
(let [v (vec-new t)]
(dotimes [i (length s)]
(when (keep? (at s i))
@@ -577,7 +577,7 @@ let source = {flan|
;; total silently wraps. The per-element (i64 ...) would happen on its own now;
;; it is written to keep the accumulator's type visible at the line that feeds
;; it.
-(defn sum-i32 [s [i32]] i64
+(defn sum-i32 [s [const i32]] i64
(let [t (i64 0)]
(dotimes [i (length s)]
(set t (+ t (i64 (at s i)))))
@@ -590,7 +590,7 @@ let source = {flan|
;; is silently short rather than obviously wrong. An f64 accumulator has 29
;; more bits of mantissa and pushes that failure out of reach of any array a
;; game holds.
-(defn sum-f32 [s [f32]] f64
+(defn sum-f32 [s [const f32]] f64
(let [t 0.0]
(dotimes [i (length s)]
(set t (+ t (f64 (at s i)))))
@@ -598,12 +598,12 @@ let source = {flan|
;; ── Bytes ─────────────────────────────────────────────────────────────
;;
-;; Over [u8] and not over string, so (bytes-view s) is what a caller writes and one
-;; copy of each serves strings and byte slices both — which is as close to a
+;; Over [const u8] and not over string, so (bytes-view s) is what a caller writes
+;; and one copy of each serves strings and byte slices both, writable or not — which is as close to a
;; generic as a language without them gets. Nothing here allocates: every
;; result is a bool, an index, or a number.
-(defn bytes=? [a [u8] b [u8]] bool
+(defn bytes=? [a [const u8] b [const u8]] bool
(if (!= (length a) (length b))
false
(do
@@ -615,11 +615,11 @@ let source = {flan|
;; The length test comes first and `and` short-circuits, so the slice is only
;; built once it is known to be in bounds — otherwise a prefix longer than the
;; string would trap rather than answer false.
-(defn starts-with? [s [u8] p [u8]] bool
+(defn starts-with? [s [const u8] p [const u8]] bool
(and (<= (length p) (length s))
(bytes=? (slice s 0 (length p)) p)))
-(defn ends-with? [s [u8] p [u8]] bool
+(defn ends-with? [s [const u8] p [const u8]] bool
(and (<= (length p) (length s))
(bytes=? (slice s (- (length s) (length p)) (length s)) p)))
@@ -630,7 +630,7 @@ let source = {flan|
;; libc-dependent, and a parser in the language gives the same answer on
;; wasm32 as on native for the same reason rand does.
;; Overflow wraps, as all arithmetic here does; it is not reported.
-(defn parse-i64 [s [u8]] (Option i64)
+(defn parse-i64 [s [const u8]] (Option i64)
(let [i 0
n (i64 0)
neg false]
@@ -1221,7 +1221,7 @@ let source = {flan|
;;
;; Naive, O(n·m), and that is the deliberate choice: Boyer–Moore wants a skip
;; table, which is an array sized by the needle, which is an allocation.
-(defn index-of-bytes [s [u8] p [u8]] (Option i32)
+(defn index-of-bytes [s [const u8] p [const u8]] (Option i32)
(when (> (length p) (length s))
(return None))
(let [last (- (length s) (length p))
@@ -1240,7 +1240,7 @@ let source = {flan|
;; The two loops both test (< lo hi), so an all-whitespace input walks lo up
;; to hi and stops there, and the result is the empty slice. Without that test
;; lo would pass hi and (slice s lo hi) would be a reversed range, which traps.
-(defn trim [s [u8]] [u8]
+(defn trim [s [const u8]] [const u8]
(let [lo 0
hi (length s)]
(while (and (< lo hi) (space? (at s lo)))
@@ -1268,7 +1268,7 @@ let source = {flan|
;; 511 cap is flan_bytes_to_f64's buffer: past it the shim truncates, and a
;; validator that said yes to 600 digits would be approving a different
;; number than the one strtod reads.
-(defn parse-f64 [s [u8]] (Option f64)
+(defn parse-f64 [s [const u8]] (Option f64)
(let [i 0
digits 0]
(when (or (= (length s) 0) (> (length s) 511))
@@ -1350,7 +1350,7 @@ let source = {flan|
(defn rune-start? [b u8] bool
(!= (bit-and b 0xc0) 0x80))
-(defn decode-rune [s [u8]] Rune
+(defn decode-rune [s [const u8]] Rune
(when (= (length s) 0)
(return (Rune {.code 0 .width 0 .ok false})))
(let [b0 (at s 0)]
@@ -1406,7 +1406,7 @@ let source = {flan|
;; Decode at a byte offset. None when the offset is not on a rune boundary or
;; the bytes there are malformed, which is stricter than Odin's rune_at — that
;; one hands back RUNE_ERROR and the caller carries on with a wrong character.
-(defn rune-at [s [u8] i i32] (Option i32)
+(defn rune-at [s [const u8] i i32] (Option i32)
(if (or (< i 0) (>= i (length s)))
None
(let [r (decode-rune (slice s i (length s)))]
@@ -1419,7 +1419,7 @@ let source = {flan|
;;
;; A malformed byte counts as one, which is what a replacement-character
;; renderer would draw, so this agrees with what the screen shows.
-(defn rune-count [s [u8]] i32
+(defn rune-count [s [const u8]] i32
(let [i 0
n 0]
(while (< i (length s))
@@ -1428,7 +1428,7 @@ let source = {flan|
(set n (+ n 1))))
n))
-(defn valid-utf8? [s [u8]] bool
+(defn valid-utf8? [s [const u8]] bool
(let [i 0]
(while (< i (length s))
(let [r (decode-rune (slice s i (length s)))]
@@ -1507,12 +1507,12 @@ let source = {flan|
;; empty field, and `rest` is exhausted only after the last one is taken. That
;; is the rule you can state without exceptions, and the one a caller counting
;; comma-separated columns needs.
-(defstruct Split [rest [u8] sep u8 more bool])
+(defstruct Split [rest [const u8] sep u8 more bool])
-(defn split-on-byte [s [u8] sep u8] Split
+(defn split-on-byte [s [const u8] sep u8] Split
(Split {.rest s .sep sep .more true}))
-(defn split-next [it (Ptr Split)] (Option [u8])
+(defn split-next [it (Ptr Split)] (Option [const u8])
(when (not (.more it))
(return None))
(match (index-of (.rest it) (.sep it))
@@ -1534,25 +1534,11 @@ let source = {flan|
;; the ones in the building section below; these are the forms that allocate
;; nothing, and they stay the right call when a copy is not wanted — folding a
;; comparison over two inputs beats lowering both and comparing. What is *not*
-;; on offer is the third shape, lowering a [u8] in place, and it is worth
-;; saying why rather than shipping it. A string
-;; literal is emitted `private unnamed_addr constant` (emit.ml), so (bytes-view
-;; "Hello") is a [u8] pointing straight into read-only memory. An in-place
-;; lower-ascii type checks against that slice, and what happens next depends
-;; on the optimiser — which is the worst of the available answers. Measured,
-;; with (set (at (bytes-view "Hi") 0) \h):
-;;
-;; -O0 the store is emitted against the constant and the program takes
-;; SIGSEGV.
-;; -O2 LLVM deletes the store as undefined behaviour and the program
-;; carries on and prints "Hi".
-;;
-;; So the same source either dies or silently does nothing depending on a
-;; flag, and the -O2 half is the quiet-wrongness class this file keeps
-;; refusing elsewhere. (bytes s) answers a writable copy now for exactly this
-;; reason; these byte functions stay the right call when no copy is wanted,
-;; and a caller that really does own its buffer writes the two-line loop
-;; itself over storage it can see the declaration of.
+;; on offer is the third shape, lowering a [u8] in place: the text a caller
+;; has is most often a (bytes-view s), which is a [const u8] because a string
+;; literal's bytes are in read-only memory, and an in-place lower could not
+;; take it. (bytes s) is the writable copy; a caller that owns its buffer
+;; writes the two-line loop itself.
;;
;; ASCII only, and only the 26 letters: case outside ASCII is not a byte
;; operation at all — it is per-code-point, it is not length-preserving (ß
@@ -1567,7 +1553,7 @@ let source = {flan|
;; Case-insensitive comparison as a fold over both inputs, which is the useful
;; half of to_lower and needs no storage at all: comparing two lowered copies
;; is what a caller wanted, and this is that answer without either copy.
-(defn bytes-ci=? [a [u8] b [u8]] bool
+(defn bytes-ci=? [a [const u8] b [const u8]] bool
(if (!= (length a) (length b))
false
(do
@@ -1579,7 +1565,7 @@ let source = {flan|
;; ── Ordering byte slices, and sorting them ────────────────────────────
;;
;; The third element type the slice family covers, and the one a caller of
-;; `split` actually has: a [[u8]] of fields, wanting to come out in order.
+;; `split` actually has: a [[const u8]] of fields, wanting to come out in order.
;;
;; The order is bytewise-lexicographic — memcmp's, and the one every sane
;; sorted format uses. It is explicitly *not* alphabetical and not a collation:
@@ -1596,7 +1582,7 @@ let source = {flan|
;; A prefix sorts before what extends it — "ab" before "abc" — which falls out
;; of running to the shorter length and then comparing lengths, and is the case
;; a loop written to (length a) alone reads off the end for.
-(defn bytes [a [u8] b [u8]] bool
+(defn bytes [a [const u8] b [const u8]] bool
(let [n (min (length a) (length b))]
(dotimes [i n]
(when (!= (at a i) (at b i))
@@ -1604,7 +1590,7 @@ let source = {flan|
(< (length a) (length b))))
;; sort-by with the comparison written in, over the same in-place contract:
-;; the *slices* move, never the bytes they point at, so this sorts a [[u8]] of
+;; the *slices* move, never the bytes they point at, so this sorts a [[const u8]] of
;; fields borrowed from one buffer without touching the buffer. Stable, and
;; here that is observable — two equal fields are two distinct slices of
;; different parts of the input, and a caller can see which one came first.
@@ -1615,7 +1601,7 @@ let source = {flan|
;; lexicographically is a loop and not an instruction. bytes is that loop.
;; So this is the shape a generic takes when the operation it needs is not a
;; primitive: pass it in.
-(defn sort-bytes [s [[u8]]] ()
+(defn sort-bytes [s [[const u8]]] ()
(sort-by s (fn [a b] (bytes a b))))
;; ── Building bytes, which is the tier that needed an allocator ────────
@@ -1654,7 +1640,7 @@ let source = {flan|
;; It takes a (Ptr (Vec u8)) and not a (Vec u8), and the difference is not
;; style: a Vec parameter *moves*, so (append b s) taking one by value would
;; consume the caller's builder on the first call and refuse the second.
-(defn append [b (Ptr (Vec u8)) s [u8]] ()
+(defn append [b (Ptr (Vec u8)) s [const u8]] ()
(dotimes [i (length s)]
(push (deref b) (at s i))))
@@ -1673,12 +1659,13 @@ let source = {flan|
;; concat and join. Both take a slice of slices, which is the shape a caller
;; already has: an array literal of them, [(bytes-view "a") (bytes-view b)], slices to a
-;; [[u8]] and copies nothing.
+;; [[const u8]] and copies nothing. The outer slice is const too, which is what
+;; lets a [[u8]] in as well: nothing here can store a read-only slice into it.
;;
;; join with an empty separator is concat, and concat is here anyway because
;; the empty (bytes-view "") a caller would have to write is the kind of argument
;; that reads like a mistake at the call site.
-(defn concat [parts [[u8]]] (Vec u8)
+(defn concat [parts [const [const u8]]] (Vec u8)
(let [b (vec-new u8)]
(dotimes [i (length parts)]
(append (addr b) (at parts i)))
@@ -1688,7 +1675,7 @@ let source = {flan|
;; result rather than a leading separator — which is the off-by-one a join
;; written as "append part then separator, then chop the tail" gets wrong on
;; exactly that input, because there is no tail to chop.
-(defn join [parts [[u8]] sep [u8]] (Vec u8)
+(defn join [parts [const [const u8]] sep [const u8]] (Vec u8)
(let [b (vec-new u8)]
(dotimes [i (length parts)]
(when (> i 0)
@@ -1696,24 +1683,21 @@ let source = {flan|
(append (addr b) (at parts i)))
b))
-(defn repeat-bytes [s [u8] n i32] (Vec u8)
+(defn repeat-bytes [s [const u8] n i32] (Vec u8)
(let [b (vec-new u8)]
(dotimes [i n]
(append (addr b) s))
b))
;; The allocating halves of the ASCII case pair. The note above lower-ascii
-;; explains why lowering a [u8] *in place* is a trap — a string literal is
-;; emitted into .rodata, so the store either segfaults at -O0 or is deleted at
-;; -O2 — and this is the shape that has no such hole: the bytes it writes are
-;; its own.
-(defn to-lower [s [u8]] (Vec u8)
+;; says why there is no in-place one; these write only bytes of their own.
+(defn to-lower [s [const u8]] (Vec u8)
(let [b (vec-new u8)]
(dotimes [i (length s)]
(push b (lower-ascii (at s i))))
b))
-(defn to-upper [s [u8]] (Vec u8)
+(defn to-upper [s [const u8]] (Vec u8)
(let [b (vec-new u8)]
(dotimes [i (length s)]
(push b (upper-ascii (at s i))))
@@ -1732,7 +1716,7 @@ let source = {flan|
;; choice: returning a Vec *moves* it, and the move analysis is a dead set over
;; the whole function, so a `return b` on one branch kills the binding for the
;; `b` at the foot of the other. One exit, one move.
-(defn replace-bytes [s [u8] from [u8] to [u8]] (Vec u8)
+(defn replace-bytes [s [const u8] from [const u8] to [const u8]] (Vec u8)
(let [b (vec-new u8)
i 0]
(if (= (length from) 0)
@@ -1757,7 +1741,7 @@ let source = {flan|
;; the other way. A return type does say it. That is a compiler gap rather than
;; a language decision, and it is written down in TODO.org, "(vec-new [u8]) is
;; refused".
-(defn slices-new [] (Vec [u8]) (vec-new))
+(defn slices-new [] (Vec [const u8]) (vec-new))
;; split, which the file used to refuse by name. The fields are slices *of the
;; input* and not copies, so nothing here owns bytes and the result dies with
@@ -1769,7 +1753,7 @@ let source = {flan|
;; always yield n+1 fields, so the empty input yields one empty field and a
;; trailing separator yields a trailing empty one. That is Odin's allocating
;; strings.split and not Odin's iterator, which disagree with each other.
-(defn split [s [u8] sep u8] (Vec [u8])
+(defn split [s [const u8] sep u8] (Vec [const u8])
(let [v (slices-new)
it (split-on-byte s sep)
going true]
@@ -1929,10 +1913,9 @@ let source = {flan|
;;
;; `data` points into the program's own .rodata, exactly as a string literal
;; does, so an embed costs nothing at run time and nothing at startup. It is
-;; also read-only, and the same trap the ASCII-case note above measures applies
-;; here: a store through it either segfaults at -O0 or is deleted at -O2. To
-;; get a mutable copy, clone the bytes into a Vec.
-(defstruct EmbedFile [name string data [u8]])
+;; also read-only, so `data` is a [const u8] and a store through it is refused
+;; at compile time. To get a writable copy, copy the bytes into a Vec.
+(defstruct EmbedFile [name string data [const u8]])
;; A linear scan, deliberately. A directory embed is tens of entries, the scan
;; is over names already in cache-warm .rodata, and the alternative — a
@@ -1943,7 +1926,7 @@ let source = {flan|
;; It takes a slice rather than the array (embed-dir) answers, because an array
;; length is part of its type and there are no generics: write
;; (embed-find (slice assets 0 (length assets)) "brush.png").
-(defn embed-find [files [EmbedFile] name string] (Option [u8])
+(defn embed-find [files [EmbedFile] name string] (Option [const u8])
(dotimes [i (length files)]
(when (bytes=? (bytes-view (.name (at files i))) (bytes-view name))
(return (Some (.data (at files i))))))
diff --git a/lib/render.ml b/lib/render.ml
index f1cb9667..d45251b6 100644
--- a/lib/render.ml
+++ b/lib/render.ml
@@ -110,7 +110,7 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis
let cast t x = { Tast.e = Tast.Prim (Tast.Cast t, [ x ]); ty = t; loc } in
let bytes_of s =
{ Tast.e = Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str s; ty = Types.String; loc } ]);
- ty = Types.Slice (Types.Int Types.U8); loc }
+ ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc }
in
let lit s = c.emit.ebytes (bytes_of s) in
let int64 n = { Tast.e = Tast.Int (n, Types.I64); ty = Types.Int Types.I64; loc } in
@@ -152,10 +152,10 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis
| Types.String ->
[ c.emit.estr
{ Tast.e = Tast.Prim (Tast.Bytes, [ e ]);
- ty = Types.Slice (Types.Int Types.U8); loc } ]
+ ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc } ]
(* Bytes are almost always text, and escaping makes the case where they are
not readable rather than a mess. *)
- | Types.Slice (Types.Int Types.U8) -> [ c.emit.estr e ]
+ | Types.Slice (_, (Types.Int Types.U8)) -> [ c.emit.estr e ]
(* An enum's members are erased to i32 before the backend sees them, so the
name has to be recovered here, from the checker's table, as a chain of
comparisons. Falling through to the number is not a failure: a value
@@ -355,7 +355,7 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis
(* A slice's length is not known until it runs, so this is the one case
that needs a loop. The slice goes into a slot first: the expression it
came from must not be evaluated once per element. *)
- | Types.Slice t ->
+ | Types.Slice (_, t) ->
let sv = c.alloc e.Tast.ty and iv = c.alloc (Types.Int Types.I32) in
let local i ty = { Tast.e = Tast.Local i; ty; loc } in
let len =
diff --git a/lib/session.ml b/lib/session.ml
index 00a4c241..2e172e5b 100644
--- a/lib/session.ml
+++ b/lib/session.ml
@@ -1074,8 +1074,8 @@ let eval ?(origin = "
[n T] is inline storage
and copies on assignment and on pass-by-value.[T] is ptr+len and owns nothing.
- Copying a slice copies the view, never the elements.[const T] is the
+ same view with no stores through it.
(addr x) takes the address of
any assignable place and gives (Ptr T). It does not extend anything's
lifetime, and keeping one past its frame is your contract to honour — there is no
@@ -518,6 +519,7 @@ notation reads as exactly one data item.
booli1string[T][const T][T] converts to one, never the reverse[n T](Vec T)(Map K V)(defstruct Cursor
- [src [u8] ; a non-owning slice
+ [src [const u8] ; a read-only, non-owning slice
pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8
@@ -829,8 +831,8 @@ its bytes.
There are two ways to see a string's bytes and the difference is whether anything
is allocated. (bytes-view s) is the string's own storage seen as a
-[u8] and costs nothing; it aliases the string, so a literal's view points
-into .rodata and writing through it traps. (bytes s) and
+[const u8] and costs nothing; it aliases the string, and a store through
+it is a compile error. (bytes s) and
(bytes s allocator) make a writable copy through the allocator — never a
hidden malloc, which is the rule every allocating operation follows. The
example above wants a view and takes one.