A slice can be read-only: bytes-view answers a [const u8], and a store through one is refused at compile time

This commit is contained in:
Joseph Ferano 2026-09-25 11:43:41 +07:00
parent 8b4c6f81df
commit 2ff1da88da
43 changed files with 581 additions and 347 deletions

View File

@ -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 rule is "annotate function signatures, infer locals", so a general annotation is a
deliberate absence. deliberate absence.
** NEXT A read-only slice type ** DONE 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=. CLOSED: [2026-09-25]
=bytes-view= is read-only by convention only — the type system cannot say a =[u8]= =[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.
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.
** TODO (slice d 1) over a dyn string is refused where (at d i) works ** 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 The typed and dyn spaces disagree about a spelling, which the standing rule

View File

@ -20,7 +20,7 @@
;; ── one by pointer. `addr` takes the address of a local; the pointer never ;; ── one by pointer. `addr` takes the address of a local; the pointer never
;; ── outlives the frame, so no allocator is involved. ;; ── outlives the frame, so no allocator is involved.
(defstruct Cursor (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 pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8 (defn peek [c (Ptr Cursor)] u8
@ -105,7 +105,7 @@
(Some lhs))) (Some lhs)))
;; ── Whole input, or nothing. Trailing junk is an error, not ignored. ── ;; ── 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 [c (Cursor {.src src})] ; pos omitted: zeroed
(let [v (some (parse-expr (addr c) 1))] (let [v (some (parse-expr (addr c) 1))]
(skip-spaces (addr c)) (skip-spaces (addr c))

View File

@ -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 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 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 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. 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 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. path and has no `--sanitize` to pass it.
@ -2794,7 +2794,7 @@ fires.
| `(clone v)` / `(clone v a)` | the only copy; assignment moves | | `(clone v)` / `(clone v a)` | the only copy; assignment moves |
| `(free v)` | consumes its argument | | `(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 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 ### 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 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. 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 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 **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 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. `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 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, *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. "(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 ## 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 `(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 (`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 `#` 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 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. 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 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 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`. `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 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 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 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`. order is filesystem-dependent and an unsorted embed would make two builds of identical sources emit different `.ll`.
Non-recursive, files only — Odin again. 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 **Read-only, and the type says so.** The slice points into `.rodata`, where a store would segfault at `-O0` and be
`-O0` and is deleted as undefined behaviour at `-O2` — the same trap the prelude's ASCII-case note measures for deleted as undefined behaviour at `-O2`, so it is a `[const u8]` and a store through it is refused at compile time.
`(bytes "Hi")`, and the same one TODO.org tracks as "Writing through a string literal". Nothing here makes it worse and To decode an asset in place, copy the bytes into a `Vec` first.
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.
**What this does not do.** `sand.flan` still calls `(rl/load-texture "brush.png")`, which hands raylib a path for **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 raylib to open. Pointing raylib at embedded bytes needs `LoadImageFromMemory` and `LoadTextureFromImage` in place of

View File

@ -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 ;; resolves and the two function types, `Fn' and `CFn'. `dyn' is
;; lowercase on purpose — it is a primitive beside `i64' and `bool', not ;; lowercase on purpose — it is a primitive beside `i64' and `bool', not
;; a container over something. ;; 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. ;; `Unit' is deliberately absent, though `Types.primitive_names' has it.
;; The resolver answers to the name because `Cimport' builds one for C's ;; 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 ;; word outright — unit is spelled `()'. Drawing it as a valid type would
;; advertise a spelling the parser rejects, which is the same reason ;; advertise a spelling the parser rejects, which is the same reason
;; `find-restart' and `await' are left out of `flan--special'. ;; `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) . font-lock-type-face)
;; A type variable, `$t', which is what a generic `defn' names its ;; A type variable, `$t', which is what a generic `defn' names its
;; parameter types with and what `{:where (ordered? $t)}' constrains. ;; parameter types with and what `{:where (ordered? $t)}' constrains.

View File

@ -489,6 +489,8 @@
"a package alias") "a package alias")
("(defn f [x int] float 1.0)" "int" font-lock-type-face ("(defn f [x int] float 1.0)" "int" font-lock-type-face
"int, the builtin alias") "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. ;; Constants that stand for themselves.
("(set done true)" "true" font-lock-constant-face "true") ("(set done true)" "true" font-lock-constant-face "true")
("(= o None)" "None" font-lock-constant-face "None") ("(= o None)" "None" font-lock-constant-face "None")

View File

@ -63,7 +63,7 @@
;; from here they are ordinary slices, bounds-checked like any other, and the ;; 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 ;; index comes from raylib's own get-glyph-index so it is in range by
;; construction. ;; 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 font-size f32 spacing f32 word-wrap? bool
tint rl/Color] () tint rl/Color] ()
(let [glyphs (rl/font-glyphs font) (let [glyphs (rl/font-glyphs font)

View File

@ -14,7 +14,7 @@ type texpr = { t : texpr_kind; tloc : Loc.t }
and texpr_kind = and texpr_kind =
| Tname of string (* i32 bool Cursor string *) | 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]] *) | Tarray of len * texpr (* [4 f32] [rows [cols u32]] *)
| Tmap of texpr * texpr (* (Map string i32) *) | Tmap of texpr * texpr (* (Map string i32) *)
| Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *) | Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *)

View File

@ -1137,7 +1137,8 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
let loc = t.Ast.tloc in let loc = t.Ast.tloc in
match t.Ast.t with match t.Ast.t with
| Ast.Tname n -> resolve_name env ~seen loc n | 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) -> | Ast.Tarray (l, e) ->
let e = resolve env ~seen e in let e = resolve env ~seen e in
no_zeroed_fn loc "a fixed array's element" e; 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 | _ -> bracket_value_element env values e
in in
match t.Ast.t with match t.Ast.t with
| Ast.Tslice e -> elem e | Ast.Tslice (_, e) -> elem e
| Ast.Tarray (_, e) -> elem e | Ast.Tarray (_, e) -> elem e
| _ -> None | _ -> None
@ -1734,7 +1735,7 @@ let signature_tyvars (fn : Ast.fn) =
let rec ty (t : Ast.texpr) = let rec ty (t : Ast.texpr) =
match t.Ast.t with match t.Ast.t with
| Ast.Tname n -> name t.Ast.tloc n | Ast.Tname n -> name t.Ast.tloc n
| Ast.Tslice e -> ty e | Ast.Tslice (_, e) -> ty e
| Ast.Tarray (_, e) -> ty e | Ast.Tarray (_, e) -> ty e
| Ast.Tmap (k, v) -> ty k; ty v | Ast.Tmap (k, v) -> ty k; ty v
(* The head of an application is a constructor — [Ptr], [Option], [Vec] — (* 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 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 bound to, so [(pair 1 2.0)] over [a $t b $t] is a refusal and not a
second instantiation. *) 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 match pat, arg with
| Types.Var v, a -> | Types.Var v, a ->
(match List.assoc_opt v !subst with (match List.assoc_opt v !subst with
| None -> subst := (v, a) :: !subst; true | None -> subst := (v, a) :: !subst; true
| Some b -> Types.equal a b) | 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.Ptr p, Types.Ptr a
| Types.Vec p, Types.Vec a | Types.Vec p, Types.Vec a
| Types.Option p, Types.Option a -> bind_ty subst p a | Types.Option p, Types.Option a -> inner p a
| Types.Array (n, p), Types.Array (m, a) -> Int64.equal n m && bind_ty subst 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') -> | Types.Map (k, v), Types.Map (k', v') -> inner k k' && inner v v'
bind_ty subst k k' && bind_ty subst v v'
(* Each function type against its own. *) (* Each function type against its own. *)
| Types.Fn (ps, r), Types.Fn (ps', r') | Types.Fn (ps, r), Types.Fn (ps', r')
| Types.CFn (ps, r), Types.CFn (ps', r') -> | Types.CFn (ps, r), Types.CFn (ps', r') ->
List.length ps = List.length ps' 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 (* And the widening between them, which is admitted at the top of an
argument's type and nowhere inside it. argument's type and nowhere inside it.
[(Fn [$t] $t)] against a [(CFn [i32] i32)] is the shape every caller of [(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. *) argument. *)
| Types.Fn (ps, r), Types.CFn (ps', r') when widen -> | Types.Fn (ps, r), Types.CFn (ps', r') when widen ->
List.length ps = List.length ps' 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 (* Nothing generic left on the pattern side: this is ordinary type
equality, and [Never] fits anywhere exactly as it does elsewhere. *) equality, and [Never] fits anywhere exactly as it does elsewhere. *)
| p, a -> Types.fits ~expected:p ~actual:a | 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) = let rec subst_ty subst (t : Types.t) =
match t with match t with
| Types.Var v -> (match List.assoc_opt v subst with Some c -> c | None -> t) | 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.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.Map (k, v) -> Types.Map (subst_ty subst k, subst_ty subst v)
| Types.Ptr e -> Types.Ptr (subst_ty subst e) | 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) = let rec generic_ty (t : Types.t) =
match t with match t with
| Types.Var _ -> true | 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.Option e -> generic_ty e
| Types.Map (k, v) -> generic_ty k || generic_ty v | Types.Map (k, v) -> generic_ty k || generic_ty v
| Types.Fn (ps, r) | Types.CFn (ps, r) -> | 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) = let rec reaches_dyn (t : Types.t) =
match t with match t with
| Types.Dyn -> true | 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.Option e -> reaches_dyn e
| Types.Map (k, v) -> reaches_dyn k || reaches_dyn v | Types.Map (k, v) -> reaches_dyn k || reaches_dyn v
| Types.Fn (ps, r) | Types.CFn (ps, r) -> | 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) = let rec mangle_ty (t : Types.t) =
match t with match t with
| Types.Unit -> "unit" | 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.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.Map (k, v) -> Printf.sprintf "map-%s-%s" (mangle_ty k) (mangle_ty v)
| Types.Ptr e -> "ptr-" ^ mangle_ty e | Types.Ptr e -> "ptr-" ^ mangle_ty e
@ -1917,7 +1925,7 @@ let rec occurs_in ~needle (t : Types.t) =
Types.equal needle t Types.equal needle t
|| ||
match t with 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.Option e -> occurs_in ~needle e
| Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v | Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v
| Types.Fn (ps, r) | Types.CFn (ps, r) -> | 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 to_bytes ctx loc pr (x : Tast.expr) =
let bty = Types.Array (num_bytes, Types.Int Types.U8) in 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 let s = fresh_slot ctx bty in
mk loc bslice mk loc bslice
(Tast.Let (Tast.Let
@ -2551,7 +2559,16 @@ let box loc (e : Tast.expr) : Tast.expr =
| Some k -> | Some k ->
if not (permanent_root e) then view_not_permanent loc e.Tast.ty 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 ]) 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 (match view_elem elem with
| None -> view_not_yet loc e.Tast.ty elem | None -> view_not_yet loc e.Tast.ty elem
| Some k -> | Some k ->
@ -2849,7 +2866,8 @@ let rec thick_enc (t : Types.t) =
| Types.Named n -> "n" ^ atom n | Types.Named n -> "n" ^ atom n
| Types.Enum n -> "e" ^ atom n | Types.Enum n -> "e" ^ atom n
| Types.Var v -> "y" ^ atom v | 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.Ptr e -> "p" ^ thick_enc e
| Types.Vec e -> "v" ^ thick_enc e | Types.Vec e -> "v" ^ thick_enc e
| Types.Option e -> "o" ^ 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)" (%s x)"
(Types.to_string want) (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) = let expect ctx loc ~want (got : Tast.expr) =
match want with match want with
| None -> got | None -> got
@ -2958,6 +2997,12 @@ let expect ctx loc ~want (got : Tast.expr) =
| Types.Fn (ps, r), Types.CFn (ps', r') | Types.Fn (ps, r), Types.CFn (ps', r')
when Types.equal (Types.Fn (ps, r)) (Types.Fn (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)) 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 | _ -> got
in in
if Types.fits ~expected:w ~actual:got.Tast.ty then got 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 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 reader who has just been told i64 and i32 are different types needs
to be told, in the same breath, which direction needed nothing. *) 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) (Types.to_string w) (Types.to_string got.Tast.ty)
(numeric_note ~want:w ~got: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. (* 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 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 = let elem_want =
match want with match want with
| Some (Types.Array (_, t)) -> Some t | Some (Types.Array (_, t)) -> Some t
| Some (Types.Slice t) -> Some t | Some (Types.Slice (_, t)) -> Some t
| _ -> None | _ -> None
in in
(* With nothing outside saying what the elements are, the first one says: (* 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 \ "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" 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 match p with
| Ast.Pvar name -> | Ast.Pvar name ->
(* Scope first, and the capture refusal only where scope did not settle (* 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 -> | None ->
Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname) Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
"%s has no field %s" sname name "%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) -> | Ast.Pindex (target, idx) ->
let target = check ctx target in let target = check ctx target in
(match target.Tast.ty with (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 let p, ty = vec_at ctx loc target idx in
Tast.Pderef p, ty 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) Tast.Pindex (target, idx), ty)
| Ast.Pderef target -> | Ast.Pderef target ->
let target = check ctx target in 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 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 [[2 string]] reaches a string at the last step and nowhere before it, so a
question asked only of [g] would miss it. *) 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 let rec go ty = function
| [] -> [], ty | [] -> [], ty
| i :: rest -> | i :: rest ->
let elem = let elem =
match ty with 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. *) (* A string indexes to its bytes, and only to read them. *)
| Types.String -> | Types.String ->
Option.iter (fun l -> refuse_string_place l ty) place; Option.iter (fun l -> refuse_string_place l ty) place;
@ -6370,7 +6460,7 @@ and not_numeric name what (a : Tast.expr) =
let text = let text =
match a.Tast.ty with match a.Tast.ty with
| Types.String -> true | Types.String -> true
| Types.Slice (Types.Int Types.U8) -> true | Types.Slice (_, (Types.Int Types.U8)) -> true
| _ -> false | _ -> false
in in
let where = a.Tast.loc in let where = a.Tast.loc in
@ -6784,7 +6874,10 @@ and type_of_expr (e : Ast.expr) : Ast.texpr option =
in in
match e.Ast.e with match e.Ast.e with
| Ast.TypeArg t -> Some t | 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 ] -> | Ast.Arr [ { Ast.e = Ast.Int n; _ }; x ] ->
Option.map (fun t -> mk (Ast.Tarray (Ast.Lint n, t))) (inner x) Option.map (fun t -> mk (Ast.Tarray (Ast.Lint n, t))) (inner x)
| Ast.Arr [ { Ast.e = Ast.Var n; _ }; 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 lo, hi
| _ -> assert false | _ -> assert false
in in
let out = fresh_slot ctx (Types.Slice elem) in let out = fresh_slot ctx (Types.Slice (Types.Mut, elem)) in
let fill = let fill =
rt loc Types.Unit "flan_vec_as_slice" 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 ] lo; hi; size_of loc elem; here loc ]
in in
expect ctx loc ~want expect ctx loc ~want
(mk loc (Types.Slice elem) (mk loc (Types.Slice (Types.Mut, elem))
(Tast.Let ([ (out, mk loc (Types.Slice elem) (Tast.Let ([ (out, mk loc (Types.Slice (Types.Mut, elem))
(Tast.Zero (Types.Slice elem))) ], (Tast.Zero (Types.Slice (Types.Mut, elem)))) ],
[ fill; mk loc (Types.Slice elem) (Tast.Local out) ]))) [ 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 (* 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. 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 ] -> | [ s ] ->
let s = check ctx s in let s = check ctx s in
(match s.Tast.ty with (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 ]) expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_kw" [ s ])
| other -> | other ->
fail loc "keyword takes a string or a [u8], found %s" 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] the round trip through the .ll. Bound with [defconst], an [embed-dir]
becomes an LLVM constant outright (emit.ml's [const]). becomes an LLVM constant outright (emit.ml's [const]).
The one sharp edge, and it is not new: the slice this hands back points The slice this hands back points into .rodata, so it is a [const u8]: a
into .rodata, so a store through it either segfaults at -O0 or is deleted store through it would segfault at -O0 and be deleted at -O2, and the
at -O2 — the same measured trap the prelude's ASCII-case note describes type refuses it at compile time instead. Copy the bytes for a writable
for (bytes-view "Hi"). Copy the bytes — (bytes s) does exactly that for a buffer. *)
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"). *)
| "embed" -> | "embed" ->
(match args with (match args with
| [ p ] | [ p; _ ] -> | [ p ] | [ p; _ ] ->
@ -8091,17 +8181,17 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| [ _; { Ast.e = Ast.Var "string"; _ } ] | [ _ ] -> () | [ _; { Ast.e = Ast.Var "string"; _ } ] | [ _ ] -> ()
| [ _; t ] -> | [ _; t ] ->
fail t.Ast.loc 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 data = read_embed_file (embed_path loc p) p.Ast.loc in
let as_string () = mk loc Types.String (Tast.Str data) 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 — 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 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 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 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. *) 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. (* Two spellings rather than one that changes type with its context.
Odin threads a type_hint everywhere and can afford (embed "p") to 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 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 ()))) | _ -> expect ctx loc ~want (as_bytes ())))
| _ -> | _ ->
fail loc 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 ─────────────────── (* ── What a macro says when it has to refuse ───────────────────
The one thing a macro could not do, written down in the prelude where 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 [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 (Tast.Make
("EmbedFile", ("EmbedFile",
[ mk loc Types.String (Tast.Str nm); [ 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 entries
in in
expect ctx loc ~want 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 path = check ctx ~want:Types.String path in
let data = byte_slice ctx data in let data = byte_slice ctx data in
let ps = fresh_slot ctx Types.String 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_ = let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) "flan_file_write" [ try_ (rt loc (Types.Int Types.I8) "flan_file_write"
[ mk loc Types.String (Tast.Local ps); [ 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 in
(* Both operands are bound before the loop so that a retry re-attempts (* 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 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 calling it a byte slice would hand out a writable-looking view of
storage the program does not own. *) storage the program does not own. *)
let result = match ty with 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 | Types.String -> Types.String
| other -> | other ->
fail loc fail loc
@ -8545,7 +8636,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
fail n_loc fail n_loc
"slice-from-ptr length %Ld is negative" k "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) | _ -> assert false)
(* ── pointers ──────────────────────────────────────────────────── *) (* ── 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) \ "addr takes the address of a place — a name, (.field x), (at a i) \
or (deref p)" or (deref p)"
| Some 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))) expect ctx loc ~want (mk loc (Types.Ptr ty) (Tast.Addr p)))
| "deref" -> | "deref" ->
arity ctx loc name 1 args; 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)) expect ctx loc ~want (mk loc (Types.Option a.Tast.ty) (Tast.Some_ a))
(* ── the milestone-2 host primitives (plan.org) ────────────────── *) (* ── the milestone-2 host primitives (plan.org) ────────────────── *)
(* (bytes-view s): the string's own storage seen as a [u8], costing nothing. (* (bytes-view s): the string's own storage seen as a [const u8], costing
This is what (bytes s) used to be, renamed for what it is: a *view*. The nothing. The slice aliases the string, and a literal's bytes are in
slice aliases the string — a literal's view points into .rodata and a read-only memory — a store through them would trap at -O0 and be deleted
store through it traps at -O0 on either backend — so it is read-only by as undefined at -O2 — so the view is one that can only be read, and a
convention until the type system can say so (TODO.org, "A read-only store through it is refused here rather than at run time. (bytes s) is
slice type"). the writable copy.
Reading through it is the whole use: bytes=?, split, index-of-bytes and Reading through it is the whole use: bytes=?, split, index-of-bytes and
every other comparison walks a string's bytes without copying them. *) every other comparison walks a string's bytes without copying them. *)
| "bytes-view" -> | "bytes-view" ->
arity ctx loc name 1 args; 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) ] [ check ctx ~want:Types.String (List.hd args) ]
(* (bytes s) / (bytes s a): a *writable copy* of the string's bytes, from (* (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. *) the string. Same rule as [push]'s element. *)
let sv = fresh_slot ctx Types.String in let sv = fresh_slot ctx Types.String in
let v = fresh_slot ctx (Types.Vec u8) 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 = let attempt =
rt loc (Types.Int Types.I8) "flan_bytes_dup" rt loc (Types.Int Types.I8) "flan_bytes_dup"
[ mk loc (Types.Vec u8) (Tast.Local v); a; [ 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 = let fill =
rt loc Types.Unit "flan_vec_as_slice" rt loc Types.Unit "flan_vec_as_slice"
[ mk loc (Types.Vec u8) (Tast.Local v); [ 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 (0L, Types.I32));
mk loc index_ty (Tast.Int (-1L, Types.I32)); mk loc index_ty (Tast.Int (-1L, Types.I32));
size_of loc u8; here loc ] size_of loc u8; here loc ]
in in
expect ctx loc ~want expect ctx loc ~want
(mk loc (Types.Slice u8) (mk loc (Types.Slice (Types.Mut, u8))
(Tast.Let (Tast.Let
([ (sv, s); ([ (sv, s);
(v, mk loc (Types.Vec u8) (Tast.Zero (Types.Vec u8))); (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) [ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec" (reg_note loc "flan_dev_reg_note_vec"
(mk loc (Types.Vec u8) (Tast.Local v)) (mk loc (Types.Vec u8) (Tast.Local v))
[ size_of loc u8 ] u8); [ size_of loc u8 ] u8);
fill; 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)") | _ -> fail loc "bytes is (bytes s) or (bytes s allocator)")
(* (string b): a [u8] seen as a string. The mirror of (bytes-view s), (* (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 would be the only enforcement point in the language — a claim the rest
of it does not make. of it does not make.
2. It does not widen the literal-write hole (TODO.org, "Writing through a 2. It takes a [const u8], so a [u8] and a (bytes-view s) are both
string literal"). That hole is the other direction: (bytes-view "Hi") accepted. This direction only loses the ability to write — a string
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
is read-only everywhere — so the result of (string b) can reach is read-only everywhere — so the result of (string b) can reach
strictly fewer stores than b could. 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 if List.exists (fun a -> generic_ty a.Tast.ty) checked then
mk loc Types.Unit Tast.Unit mk loc Types.Unit Tast.Unit
else 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 let write x = mk loc Types.Unit (Tast.Prim (Tast.WriteStdout, [ x ])) in
(* One frame slot per conversion the printer emits, which is what (* One frame slot per conversion the printer emits, which is what
[to_bytes] is for. The printer writes each number out before making the [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 rc = render_ctx ctx emitter in
let render_one a = let render_one a =
match a.Tast.ty with 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 ]))) ] [ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ]
| _ -> Render.render rc 0 a | _ -> Render.render rc 0 a
in 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 if generic_ty v.Tast.ty then mk loc Types.Unit Tast.Unit
else begin else begin
let unit_rt sym args = mk loc Types.Unit (Tast.Prim (Tast.Rt sym, args)) in 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 = let emitter : Render.emitter =
{ Render.ebytes = (fun x -> unit_rt "flan_dev_watch_emit" [ x ]); { Render.ebytes = (fun x -> unit_rt "flan_dev_watch_emit" [ x ]);
estr = (fun x -> unit_rt "flan_dev_watch_emit_str" [ 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) ] prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
| "argv" -> | "argv" ->
arity ctx loc name 0 args; 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 ──────────────── (* ── 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) = let rec mentions v (t : Types.t) =
match t with match t with
| Types.Var u -> String.equal u v | 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.Option e -> mentions v e
| Types.Map (k, w) -> mentions v k || mentions v w | Types.Map (k, w) -> mentions v k || mentions v w
| Types.Fn (ps, r) | Types.CFn (ps, r) -> | 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 (* [~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]. *) 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 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 fail a.Tast.loc "%s expects %s here, found %s%s" name
(Types.to_string p) (Types.to_string a.Tast.ty); (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) a)
pats args pats args
in in
@ -9792,7 +9887,7 @@ and is_cast name =
Types.ikind_of_name name <> None || Types.fkind_of_name name <> None Types.ikind_of_name name <> None || Types.fkind_of_name name <> None
and byte_slice ctx (a : Ast.expr) = 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 = and numeric_want want =
match want with Some (Types.Int _ | Types.Float _) -> want | _ -> None 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 \ 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) \ call over the first argument and a defmulti whose body is (class-of x) \
is the same generic function written the other way."); 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 \ "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 \ exists at run time — a reader building :texture-path out of a token's \
text. A literal :foo is already one."); text. A literal :foo is already one.");
(* assets, embedded at compile time *) (* 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; \ "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 \ (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 \ 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 \ "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 FileError under retry and use-value, and a failure to allocate \
signals StorageExhausted."); signals StorageExhausted.");
("barf", "barf [string [u8]] ()", ("barf", "barf [string [const u8]] ()",
"Writes a whole file. On the web target it signals FileError every \ "Writes a whole file. On the web target it signals FileError every \
time, with the path — there is no conditional compilation, so the \ time, with the path — there is no conditional compilation, so the \
program decides rather than the build."); 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 \ StorageExhausted with retry — and the block lives until its \
allocator's free-all or destroy. For reading without a copy, \ allocator's free-all or destroy. For reading without a copy, \
bytes-view."); bytes-view.");
("bytes-view", "bytes-view [string] [u8]", ("bytes-view", "bytes-view [string] [const u8]",
"The string's own storage seen as a byte slice. It costs nothing — both \ "The string's own storage seen as a read-only byte slice. It costs \
are a ptr and a length at run time — and it decodes nothing. \ nothing — both are a ptr and a length at run time — and it decodes \
Read-only by convention: a literal's bytes are constant data, so the \ nothing. A store through it is a compile error; bytes is the writable \
slice looks writable and a store through it traps."); copy.");
("string", "string [[u8]] string", ("string", "string [[const u8]] string",
"A byte slice seen as a string, and free at run time. It does not check \ "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 \ UTF-8, because `string` does not claim UTF-8 — valid-utf8? is an \
ordinary function you call when you care."); ordinary function you call when you care.");
("bytes->f64", "bytes->f64 [[u8]] f64", "Parses a float out of the bytes."); ("bytes->f64", "bytes->f64 [[const u8]] f64", "Parses a float out of the bytes.");
("bytes->i64", "bytes->i64 [[u8]] i64", ("bytes->i64", "bytes->i64 [[const u8]] i64",
"Parses an integer out of the bytes."); "Parses an integer out of the bytes.");
("f64->bytes", "f64->bytes [f64] [u8]", ("f64->bytes", "f64->bytes [f64] [u8]",
"The number's text, in a frame slot belonging to this call site — so \ "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]", ("i64->bytes", "i64->bytes [i64] [u8]",
"The number's text, in a frame slot belonging to this call site; it \ "The number's text, in a frame slot belonging to this call site; it \
does not survive the frame."); 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 \ "Writes the bytes to standard output exactly as given: no newline and \
no formatting."); no formatting.");
("print", "print [T ...] ()", ("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 \ spelled with it reaches the compiler's builtins and never a \
declaration — nothing could call this one" declaration — nothing could call this one"
n builtin_prefix 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; decls;
let claimed = Hashtbl.create 64 in let claimed = Hashtbl.create 64 in
@ -11501,7 +11605,7 @@ let check_main env decls =
let ok_params = let ok_params =
match params with match params with
| [] -> true | [] -> true
| [ Types.Slice Types.String ] -> true | [ Types.Slice (_, Types.String) ] -> true
| _ -> false | _ -> false
in in
if not ok_params then if not ok_params then
@ -11750,7 +11854,7 @@ let rec dyn_reach ~through p seen (t : Types.t) =
| Types.Dyn -> true | Types.Dyn -> true
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e | Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v | 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.Fn _ -> false
| Types.Named n when not (List.mem n seen) -> | Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in 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] It still terminates. This walk's own [seen] guards its own [Named]
recursion, and each crossing starts a separate finite walk of its own. *) 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.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v | Types.Map (k, v) -> go k || go v
| Types.Dyn | Types.Fn _ -> false | 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 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 (* 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. *) 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.Fn _ -> None
| Types.Named n when not (List.mem n seen) -> | Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in 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 foreign parameter of pointer or slice type receives is the address
of a place. Below it the question is [dyn_behind_pointer]'s again. *) of a place. Below it the question is [dyn_behind_pointer]'s again. *)
let below (t : Types.t) = 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 in
List.iteri List.iteri
(fun i t -> (fun i t ->

View File

@ -408,7 +408,8 @@ let rec ty_source (t : Ast.texpr) =
| Ast.Tname n -> n | Ast.Tname n -> n
| Ast.Tapp (n, args) -> | Ast.Tapp (n, args) ->
Printf.sprintf "(%s %s)" n (String.concat " " (List.map ty_source 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.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.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (ty_source e)
| Ast.Tmap (k, v) -> | Ast.Tmap (k, v) ->

View File

@ -765,7 +765,7 @@ let rec dty m d (t : Types.t) : int =
| Types.String -> | Types.String ->
composite "string" composite "string"
[ ("ptr", Types.Ptr (Types.Int Types.U8)); ("len", Types.Int Types.I64) ] [ ("ptr", Types.Ptr (Types.Int Types.U8)); ("len", Types.Int Types.I64) ]
| Types.Slice e -> | Types.Slice (_, e) ->
composite (Types.to_string t) composite (Types.to_string t)
[ ("ptr", Types.Ptr e); ("len", Types.Int Types.I64) ] [ ("ptr", Types.Ptr e); ("len", Types.Int Types.I64) ]
| Types.Option e -> | Types.Option e ->
@ -2488,7 +2488,7 @@ and element_addr f (target : Tast.expr) idx =
same way, bounds check included. *) same way, bounds check included. *)
| Types.Slice _ | Types.String -> | Types.Slice _ | Types.String ->
let elem = 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. *) (* A slice is ptr+len, so step through the pointer it holds. *)
let s = load f ptr ty in let s = load f ptr ty in
let base = fresh f 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" ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
p (ll target.Tast.ty) a lo64; p (ll target.Tast.ty) a lo64;
p p
| Types.Slice elem -> | Types.Slice (_, elem) ->
let v = value f target in let v = value f target in
let q = fresh f in let q = fresh f in
ins f "%s = extractvalue %%slice %s, 0" q v; 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"; term f "unreachable";
"zeroinitializer" "zeroinitializer"
| Tast.Argv, [] -> | 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; 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 (* 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 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. *) 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) = and shim_out f name (x : Tast.expr) (buf : Tast.expr) =
let v = value f x in let v = value f x in
let b = value f buf 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; 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 (* Slice in, slice out: [shim_in] returns a scalar and [shim_out] takes one, so
a shim that transforms bytes into bytes is neither. *) a shim that transforms bytes into bytes is neither. *)
and shim_in_out f name (x : Tast.expr) = and shim_in_out f name (x : Tast.expr) =
let p, n = explode f x in 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; 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 = and cast f ~guard (x : Tast.expr) target =
let v = value f x in let v = value f x in

View File

@ -219,7 +219,7 @@ let rec refuse_ty loc (t : Types.t) =
match t with match t with
| Types.Int _ | Types.Float _ | Types.Bool | Types.String | Types.Unit | Types.Int _ | Types.Float _ | Types.Bool | Types.String | Types.Unit
| Types.Never | Types.Named _ | Types.Enum _ -> () | 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.Vec t -> refuse_ty loc t
| Types.Fn (ps, r) | Types.CFn (ps, r) -> | Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.iter (refuse_ty loc) ps; refuse_ty loc 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) = let elem_ty loc (t : Types.t) =
match t with match t with
| Types.Slice e | Types.Array (_, e) -> e | Types.Slice (_, e) | Types.Array (_, e) -> e
| Types.String -> Types.Int Types.U8 | Types.String -> Types.Int Types.U8
| t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t) | t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t)

View File

@ -198,7 +198,7 @@ let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr =
match t.Ast.t with match t.Ast.t with
| Ast.Tname n when List.mem n owned -> Ast.Tname (qualify alias n) | Ast.Tname n when List.mem n owned -> Ast.Tname (qualify alias n)
| Ast.Tname _ as k -> k | 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 (* The length too: [rows] in [[rows [cols u32]]] is an ordinary
compile-time constant of the package, not part of the type syntax. *) compile-time constant of the package, not part of the type syntax. *)
| Ast.Tarray (l, e) -> | Ast.Tarray (l, e) ->
@ -742,7 +742,7 @@ let exported n = not (String.equal n "main")
let rec texpr_uses acc (t : Ast.texpr) = let rec texpr_uses acc (t : Ast.texpr) =
match t.Ast.t with match t.Ast.t with
| Ast.Tname n -> acc := (n, t.Ast.tloc) :: !acc | 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) -> | Ast.Tarray (l, e) ->
(match l with Ast.Lname n -> acc := (n, t.Ast.tloc) :: !acc | Ast.Lint _ -> ()); (match l with Ast.Lname n -> acc := (n, t.Ast.tloc) :: !acc | Ast.Lint _ -> ());
texpr_uses acc e texpr_uses acc e

View File

@ -89,10 +89,23 @@ let rec texpr (f : Form.t) : Ast.texpr =
emitter go on speaking. *) emitter go on speaking. *)
| Sym "Unit" -> fail f "unit is written (), not Unit" | Sym "Unit" -> fail f "unit is written (), not Unit"
| Sym s -> mk (Ast.Tname s) | 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 [ 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 _ -> | 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 (* 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 resolved to the same thing; the brace spelling is withdrawn, and the
refusal names the surviving one rather than letting the form fall through 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 what keeps the compiler's own parameter out of the way of
every name the author might bind. Same trick as [gensym]. *) every name the author might bind. Same trick as [gensym]. *)
params = [ { Ast.fname = macro_args; 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 } ]; floc = ps.loc } ];
(* Written out, not deferred: a macro takes [[Form]] and (* Written out, not deferred: a macro takes [[Form]] and
returns a [Form], and neither half of that is the user's to returns a [Form], and neither half of that is the user's to

View File

@ -274,7 +274,7 @@ let source = {flan|
;; One family per element type, because there are no generics: each of these ;; 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 ;; 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 ;; 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 ;; 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 ;; 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 ;; 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. ;; 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)} {:where (equal? $t)}
(dotimes [i (length s)] (dotimes [i (length s)]
(when (= (at s i) x) (when (= (at s i) x)
@ -406,7 +406,7 @@ let source = {flan|
;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+] ;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+]
;; either. These reduce a slice, which is a different operation with a ;; either. These reduce a slice, which is a different operation with a
;; different arity, so the different name is honest rather than a workaround. ;; 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)} {:where (ordered? $t)}
(if (= (length s) 0) (if (= (length s) 0)
None None
@ -415,7 +415,7 @@ let source = {flan|
(set m (min m (at s i)))) (set m (min m (at s i))))
(Some m)))) (Some m))))
(defn max-of [s [$t]] (Option $t) (defn max-of [s [const $t]] (Option $t)
{:where (ordered? $t)} {:where (ordered? $t)}
(if (= (length s) 0) (if (= (length s) 0)
None None
@ -500,7 +500,7 @@ let source = {flan|
;; The general fold, of which sum-i32 is the special case with the + written ;; 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 ;; 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. ;; 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] (let [acc init]
(dotimes [i (length s)] (dotimes [i (length s)]
(set acc (f acc (at s i)))) (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 ;; 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 ;; runtime needed no change at all, because SizeOf and AlignOf are computed at
;; the instantiation site, where the element type is concrete. ;; 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)] (let [v (vec-new t)]
(dotimes [i (length s)] (dotimes [i (length s)]
(when (keep? (at s i)) (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; ;; 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 is written to keep the accumulator's type visible at the line that feeds
;; it. ;; it.
(defn sum-i32 [s [i32]] i64 (defn sum-i32 [s [const i32]] i64
(let [t (i64 0)] (let [t (i64 0)]
(dotimes [i (length s)] (dotimes [i (length s)]
(set t (+ t (i64 (at s i))))) (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 ;; 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 ;; more bits of mantissa and pushes that failure out of reach of any array a
;; game holds. ;; game holds.
(defn sum-f32 [s [f32]] f64 (defn sum-f32 [s [const f32]] f64
(let [t 0.0] (let [t 0.0]
(dotimes [i (length s)] (dotimes [i (length s)]
(set t (+ t (f64 (at s i))))) (set t (+ t (f64 (at s i)))))
@ -598,12 +598,12 @@ let source = {flan|
;; ── Bytes ───────────────────────────────────────────────────────────── ;; ── Bytes ─────────────────────────────────────────────────────────────
;; ;;
;; Over [u8] and not over string, so (bytes-view s) is what a caller writes and one ;; Over [const u8] and not over string, so (bytes-view s) is what a caller writes
;; copy of each serves strings and byte slices both — which is as close to a ;; 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 ;; generic as a language without them gets. Nothing here allocates: every
;; result is a bool, an index, or a number. ;; 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)) (if (!= (length a) (length b))
false false
(do (do
@ -615,11 +615,11 @@ let source = {flan|
;; The length test comes first and `and` short-circuits, so the slice is only ;; 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 ;; built once it is known to be in bounds — otherwise a prefix longer than the
;; string would trap rather than answer false. ;; 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)) (and (<= (length p) (length s))
(bytes=? (slice s 0 (length p)) p))) (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)) (and (<= (length p) (length s))
(bytes=? (slice s (- (length s) (length p)) (length s)) p))) (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 ;; libc-dependent, and a parser in the language gives the same answer on
;; wasm32 as on native for the same reason rand does. ;; wasm32 as on native for the same reason rand does.
;; Overflow wraps, as all arithmetic here does; it is not reported. ;; 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 (let [i 0
n (i64 0) n (i64 0)
neg false] neg false]
@ -1221,7 +1221,7 @@ let source = {flan|
;; ;;
;; Naive, O(n·m), and that is the deliberate choice: Boyer–Moore wants a skip ;; 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. ;; 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)) (when (> (length p) (length s))
(return None)) (return None))
(let [last (- (length s) (length p)) (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 ;; 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 ;; 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. ;; 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 (let [lo 0
hi (length s)] hi (length s)]
(while (and (< lo hi) (space? (at s lo))) (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 ;; 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 ;; validator that said yes to 600 digits would be approving a different
;; number than the one strtod reads. ;; number than the one strtod reads.
(defn parse-f64 [s [u8]] (Option f64) (defn parse-f64 [s [const u8]] (Option f64)
(let [i 0 (let [i 0
digits 0] digits 0]
(when (or (= (length s) 0) (> (length s) 511)) (when (or (= (length s) 0) (> (length s) 511))
@ -1350,7 +1350,7 @@ let source = {flan|
(defn rune-start? [b u8] bool (defn rune-start? [b u8] bool
(!= (bit-and b 0xc0) 0x80)) (!= (bit-and b 0xc0) 0x80))
(defn decode-rune [s [u8]] Rune (defn decode-rune [s [const u8]] Rune
(when (= (length s) 0) (when (= (length s) 0)
(return (Rune {.code 0 .width 0 .ok false}))) (return (Rune {.code 0 .width 0 .ok false})))
(let [b0 (at s 0)] (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 ;; 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 ;; 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. ;; 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))) (if (or (< i 0) (>= i (length s)))
None None
(let [r (decode-rune (slice s i (length s)))] (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 ;; A malformed byte counts as one, which is what a replacement-character
;; renderer would draw, so this agrees with what the screen shows. ;; 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 (let [i 0
n 0] n 0]
(while (< i (length s)) (while (< i (length s))
@ -1428,7 +1428,7 @@ let source = {flan|
(set n (+ n 1)))) (set n (+ n 1))))
n)) n))
(defn valid-utf8? [s [u8]] bool (defn valid-utf8? [s [const u8]] bool
(let [i 0] (let [i 0]
(while (< i (length s)) (while (< i (length s))
(let [r (decode-rune (slice s 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 ;; 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 ;; is the rule you can state without exceptions, and the one a caller counting
;; comma-separated columns needs. ;; 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})) (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)) (when (not (.more it))
(return None)) (return None))
(match (index-of (.rest it) (.sep it)) (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 ;; 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 ;; 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* ;; 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 ;; on offer is the third shape, lowering a [u8] in place: the text a caller
;; saying why rather than shipping it. A string ;; has is most often a (bytes-view s), which is a [const u8] because a string
;; literal is emitted `private unnamed_addr constant` (emit.ml), so (bytes-view ;; literal's bytes are in read-only memory, and an in-place lower could not
;; "Hello") is a [u8] pointing straight into read-only memory. An in-place ;; take it. (bytes s) is the writable copy; a caller that owns its buffer
;; lower-ascii type checks against that slice, and what happens next depends ;; writes the two-line loop itself.
;; 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.
;; ;;
;; ASCII only, and only the 26 letters: case outside ASCII is not a byte ;; 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 (ß ;; 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 ;; 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 ;; 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. ;; 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)) (if (!= (length a) (length b))
false false
(do (do
@ -1579,7 +1565,7 @@ let source = {flan|
;; ── Ordering byte slices, and sorting them ──────────────────────────── ;; ── Ordering byte slices, and sorting them ────────────────────────────
;; ;;
;; The third element type the slice family covers, and the one a caller of ;; 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 ;; The order is bytewise-lexicographic — memcmp's, and the one every sane
;; sorted format uses. It is explicitly *not* alphabetical and not a collation: ;; 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 ;; 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 ;; 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. ;; 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))] (let [n (min (length a) (length b))]
(dotimes [i n] (dotimes [i n]
(when (!= (at a i) (at b i)) (when (!= (at a i) (at b i))
@ -1604,7 +1590,7 @@ let source = {flan|
(< (length a) (length b)))) (< (length a) (length b))))
;; sort-by with the comparison written in, over the same in-place contract: ;; 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 ;; fields borrowed from one buffer without touching the buffer. Stable, and
;; here that is observable — two equal fields are two distinct slices of ;; 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. ;; 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. ;; 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 ;; So this is the shape a generic takes when the operation it needs is not a
;; primitive: pass it in. ;; primitive: pass it in.
(defn sort-bytes [s [[u8]]] () (defn sort-bytes [s [[const u8]]] ()
(sort-by s (fn [a b] (bytes<? a b)))) (sort-by s (fn [a b] (bytes<? a b))))
;; ── Building bytes, which is the tier that needed an allocator ──────── ;; ── 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 ;; 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 ;; 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. ;; 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)] (dotimes [i (length s)]
(push (deref b) (at s i)))) (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 ;; 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 ;; 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 ;; 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 ;; the empty (bytes-view "") a caller would have to write is the kind of argument
;; that reads like a mistake at the call site. ;; 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)] (let [b (vec-new u8)]
(dotimes [i (length parts)] (dotimes [i (length parts)]
(append (addr b) (at parts i))) (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 ;; 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 ;; written as "append part then separator, then chop the tail" gets wrong on
;; exactly that input, because there is no tail to chop. ;; 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)] (let [b (vec-new u8)]
(dotimes [i (length parts)] (dotimes [i (length parts)]
(when (> i 0) (when (> i 0)
@ -1696,24 +1683,21 @@ let source = {flan|
(append (addr b) (at parts i))) (append (addr b) (at parts i)))
b)) 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)] (let [b (vec-new u8)]
(dotimes [i n] (dotimes [i n]
(append (addr b) s)) (append (addr b) s))
b)) b))
;; The allocating halves of the ASCII case pair. The note above lower-ascii ;; 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 ;; says why there is no in-place one; these write only bytes of their own.
;; emitted into .rodata, so the store either segfaults at -O0 or is deleted at (defn to-lower [s [const u8]] (Vec u8)
;; -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)
(let [b (vec-new u8)] (let [b (vec-new u8)]
(dotimes [i (length s)] (dotimes [i (length s)]
(push b (lower-ascii (at s i)))) (push b (lower-ascii (at s i))))
b)) b))
(defn to-upper [s [u8]] (Vec u8) (defn to-upper [s [const u8]] (Vec u8)
(let [b (vec-new u8)] (let [b (vec-new u8)]
(dotimes [i (length s)] (dotimes [i (length s)]
(push b (upper-ascii (at s i)))) (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 ;; 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 ;; 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. ;; `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) (let [b (vec-new u8)
i 0] i 0]
(if (= (length from) 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 ;; 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 ;; a language decision, and it is written down in TODO.org, "(vec-new [u8]) is
;; refused". ;; 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 ;; 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 ;; 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 ;; 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 ;; trailing separator yields a trailing empty one. That is Odin's allocating
;; strings.split and not Odin's iterator, which disagree with each other. ;; 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) (let [v (slices-new)
it (split-on-byte s sep) it (split-on-byte s sep)
going true] going true]
@ -1929,10 +1913,9 @@ let source = {flan|
;; ;;
;; `data` points into the program's own .rodata, exactly as a string literal ;; `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 ;; 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 ;; also read-only, so `data` is a [const u8] and a store through it is refused
;; here: a store through it either segfaults at -O0 or is deleted at -O2. To ;; at compile time. To get a writable copy, copy the bytes into a Vec.
;; get a mutable copy, clone the bytes into a Vec. (defstruct EmbedFile [name string data [const u8]])
(defstruct EmbedFile [name string data [u8]])
;; A linear scan, deliberately. A directory embed is tens of entries, the scan ;; 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 ;; 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 ;; 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 ;; length is part of its type and there are no generics: write
;; (embed-find (slice assets 0 (length assets)) "brush.png"). ;; (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)] (dotimes [i (length files)]
(when (bytes=? (bytes-view (.name (at files i))) (bytes-view name)) (when (bytes=? (bytes-view (.name (at files i))) (bytes-view name))
(return (Some (.data (at files i)))))) (return (Some (.data (at files i))))))

View File

@ -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 cast t x = { Tast.e = Tast.Prim (Tast.Cast t, [ x ]); ty = t; loc } in
let bytes_of s = let bytes_of s =
{ Tast.e = Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str s; ty = Types.String; loc } ]); { 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 in
let lit s = c.emit.ebytes (bytes_of s) 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 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 -> | Types.String ->
[ c.emit.estr [ c.emit.estr
{ Tast.e = Tast.Prim (Tast.Bytes, [ e ]); { 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 (* Bytes are almost always text, and escaping makes the case where they are
not readable rather than a mess. *) 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 (* 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 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 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 (* 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 that needs a loop. The slice goes into a slot first: the expression it
came from must not be evaluated once per element. *) 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 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 local i ty = { Tast.e = Tast.Local i; ty; loc } in
let len = let len =

View File

@ -1074,8 +1074,8 @@ let eval ?(origin = "<eval>") ?pause t src : change =
type emitter = { ename : string; ety : Types.t } type emitter = { ename : string; ety : Types.t }
let emit_bytes = { ename = "flan/dev-emit"; ety = Types.Slice (Types.Int Types.U8) } let emit_bytes = { ename = "flan/dev-emit"; ety = Types.Slice (Types.Mut, (Types.Int Types.U8)) }
let emit_str = { ename = "flan/dev-emit-str"; ety = Types.Slice (Types.Int Types.U8) } let emit_str = { ename = "flan/dev-emit-str"; ety = Types.Slice (Types.Mut, (Types.Int Types.U8)) }
let emit_i64 = { ename = "flan/dev-emit-i64"; ety = Types.Int Types.I64 } let emit_i64 = { ename = "flan/dev-emit-i64"; ety = Types.Int Types.I64 }
let emit_u64 = { ename = "flan/dev-emit-u64"; ety = Types.Int Types.U64 } let emit_u64 = { ename = "flan/dev-emit-u64"; ety = Types.Int Types.U64 }
let emit_f64 = { ename = "flan/dev-emit-f64"; ety = Types.Float Types.F64 } let emit_f64 = { ename = "flan/dev-emit-f64"; ety = Types.Float Types.F64 }
@ -1289,7 +1289,7 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
let bytes_of str = let bytes_of str =
{ Tast.e = { Tast.e =
Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]); Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]);
ty = Types.Slice (Types.Int Types.U8); loc } ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc }
in in
let lit str = c.Render.emit.Render.ebytes (bytes_of str) in let lit str = c.Render.emit.Render.ebytes (bytes_of str) in
let refused = ref [] in let refused = ref [] in
@ -1401,7 +1401,7 @@ let render_condition t ~(st : Tast.structure) : change * (string * string) list
let bytes_of str = let bytes_of str =
{ Tast.e = { Tast.e =
Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]); Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]);
ty = Types.Slice (Types.Int Types.U8); loc } ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc }
in in
let lit str = c.Render.emit.Render.ebytes (bytes_of str) in let lit str = c.Render.emit.Render.ebytes (bytes_of str) in
let refused = ref [] in let refused = ref [] in
@ -1520,7 +1520,7 @@ let step_into t (v : Tast.expr) (s : step) : (Tast.expr, string) result =
{ Tast.e = Tast.Int (Int64.of_int i, Types.I32); { Tast.e = Tast.Int (Int64.of_int i, Types.I32);
ty = Types.Int Types.I32; loc } ]); ty = Types.Int Types.I32; loc } ]);
ty = el; loc } ty = el; loc }
| Types.Slice el -> | Types.Slice (_, el) ->
(* A slice's length is not in its type, so this is the one step whose (* A slice's length is not in its type, so this is the one step whose
range cannot be settled here. It is checked in the program, like range cannot be settled here. It is checked in the program, like
every other index in a dev build. *) every other index in a dev build. *)
@ -1708,7 +1708,7 @@ let render_slot ?(origin = "<inspect>") t ~frame ~(fn : Tast.fn) ~slot ~path
Tast.Prim Tast.Prim
(Tast.Bytes, (Tast.Bytes,
[ { Tast.e = Tast.Str "\n"; ty = Types.String; loc } ]); [ { Tast.e = Tast.Str "\n"; ty = Types.String; loc } ]);
ty = Types.Slice (Types.Int Types.U8); loc } ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc }
in in
dev_emitter.Render.ei64 addr :: dev_emitter.Render.ebytes newline dev_emitter.Render.ei64 addr :: dev_emitter.Render.ebytes newline
:: parts :: parts
@ -2062,7 +2062,7 @@ let render_globals ?(origin = "<globals>") t ~(globals : Tast.global list)
let bytes_of str = let bytes_of str =
{ Tast.e = { Tast.e =
Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]); Tast.Prim (Tast.Bytes, [ { Tast.e = Tast.Str str; ty = Types.String; loc } ]);
ty = Types.Slice (Types.Int Types.U8); loc } ty = Types.Slice (Types.Mut, (Types.Int Types.U8)); loc }
in in
let lit str = c.Render.emit.Render.ebytes (bytes_of str) in let lit str = c.Render.emit.Render.ebytes (bytes_of str) in
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in

View File

@ -18,6 +18,17 @@ type ikind = I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64
type fkind = F32 | F64 type fkind = F32 | F64
(* Whether a slice may be stored through. [[const T]] is a view that can only
be read: [bytes-view] answers one, because its bytes are a string's and a
string literal's are in read-only memory. A [[T]] converts to a
[[const T]] implicitly and never back — see [const_widens] at the bottom of
this file — so every writable view is also a readable one, and a
read-only one cannot be laundered into a writable one. The const is
shallow: a [[const [u8]]] may not have its elements replaced, but each
element is a writable [[u8]] of its own. Both are the same two words at
run time; only the checker reads the flag. *)
type access = Mut | Const
type t = type t =
| Int of ikind | Int of ikind
| Float of fkind | Float of fkind
@ -29,7 +40,7 @@ type t =
(* A C enum: an i32 at run time, but its own type, so a keyword at a call (* A C enum: an i32 at run time, but its own type, so a keyword at a call
site has something to resolve against and a plain integer does not fit. *) site has something to resolve against and a plain integer does not fit. *)
| Enum of string | Enum of string
| Slice of t (* [T] ptr+len, non-owning *) | Slice of access * t (* [T] [const T] ptr+len, non-owning *)
| Array of int64 * t (* [n T] inline, a value, copies *) | Array of int64 * t (* [n T] inline, a value, copies *)
| Map of t * t (* (Map K V) *) | Map of t * t (* (Map K V) *)
| Ptr of t (* (Ptr T) *) | Ptr of t (* (Ptr T) *)
@ -179,7 +190,7 @@ let rec equal a b =
one dyn type the way there is one string type. *) one dyn type the way there is one string type. *)
| Bool, Bool | String, String | Unit, Unit | Never, Never | Dyn, Dyn -> true | Bool, Bool | String, String | Unit, Unit | Never, Never | Dyn, Dyn -> true
| Named x, Named y | Enum x, Enum y -> String.equal x y | Named x, Named y | Enum x, Enum y -> String.equal x y
| Slice x, Slice y -> equal x y | Slice (a, x), Slice (b, y) -> a = b && equal x y
| Array (n, x), Array (m, y) -> Int64.equal n m && equal x y | Array (n, x), Array (m, y) -> Int64.equal n m && equal x y
| Map (k, v), Map (k', v') -> equal k k' && equal v v' | Map (k, v), Map (k', v') -> equal k k' && equal v v'
| Ptr x, Ptr y -> equal x y | Ptr x, Ptr y -> equal x y
@ -204,7 +215,8 @@ let rec to_string = function
| Unit -> "()" | Unit -> "()"
| Never -> "Never" | Never -> "Never"
| Named n | Enum n -> n | Named n | Enum n -> n
| Slice t -> "[" ^ to_string t ^ "]" | Slice (Mut, t) -> "[" ^ to_string t ^ "]"
| Slice (Const, t) -> "[const " ^ to_string t ^ "]"
| Array (n, t) -> Printf.sprintf "[%Ld %s]" n (to_string t) | Array (n, t) -> Printf.sprintf "[%Ld %s]" n (to_string t)
| Map (k, v) -> Printf.sprintf "(Map %s %s)" (to_string k) (to_string v) | Map (k, v) -> Printf.sprintf "(Map %s %s)" (to_string k) (to_string v)
| Ptr t -> "(Ptr " ^ to_string t ^ ")" | Ptr t -> "(Ptr " ^ to_string t ^ ")"
@ -318,3 +330,19 @@ let join a b =
else if widens_to ~from:a ~into:b then Some b else if widens_to ~from:a ~into:b then Some b
else if widens_to ~from:b ~into:a then Some a else if widens_to ~from:b ~into:a then Some a
else None else None
(* The one conversion between the two slice types, and it goes one way: a
[[T]] may be seen as a [[const T]], because a view that can only be read
asks less of its bytes than one that can be written. Under a const slice
the same holds one level down — [[[u8]]] reads as [[const [const u8]]] —
because nothing can be stored through the outer view to put a read-only
slice where the writable original expects a writable one. Under a writable
slice it does not: a [[[u8]]] seen as [[[const u8]]] could have a
read-only slice stored into it and read back out as a [[u8]]. Like
[widens_to] this is a predicate and not a loosening of [equal]; the
caller is [Check.expect], which retypes the value — the two words are the
same at run time. *)
let rec const_widens ~(from : t) ~(into : t) =
match from, into with
| Slice (_, a), Slice (Const, b) -> equal a b || const_widens ~from:a ~into:b
| _ -> false

View File

@ -2540,7 +2540,7 @@ and elements f (base : loc) (ty : Types.t) (is : Tast.expr list) : loc =
| i :: rest -> | i :: rest ->
let elem = let elem =
match ty with match ty with
| Types.Array (_, el) | Types.Slice el | Types.Ptr el -> el | Types.Array (_, el) | Types.Slice (_, el) | Types.Ptr el -> el
| Types.String -> Types.Int Types.U8 | Types.String -> Types.Int Types.U8
| t -> unsupported "index into %s" (Types.to_string t) | t -> unsupported "index into %s" (Types.to_string t)
in in
@ -2838,7 +2838,7 @@ and check_cast f (loc : Loc.t) (src : Types.fkind) (k : Types.ikind) =
and element f (base : loc) (ty : Types.t) (i : Tast.expr) : loc = and element f (base : loc) (ty : Types.t) (i : Tast.expr) : loc =
let elem = let elem =
match ty with match ty with
| Types.Array (_, el) | Types.Slice el | Types.Ptr el -> el | Types.Array (_, el) | Types.Slice (_, el) | Types.Ptr el -> el
| Types.String -> Types.Int Types.U8 | Types.String -> Types.Int Types.U8
| t -> unsupported "index into %s" (Types.to_string t) | t -> unsupported "index into %s" (Types.to_string t)
in in
@ -3212,7 +3212,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst =
| Tast.Slice, [ a; lo; hi ] -> | Tast.Slice, [ a; lo; hi ] ->
let elem = let elem =
match a.Tast.ty with match a.Tast.ty with
| Types.Array (_, el) | Types.Slice el -> el | Types.Array (_, el) | Types.Slice (_, el) -> el
| Types.String -> Types.Int Types.U8 | Types.String -> Types.Int Types.U8
| ty -> unsupported "slice of %s" (Types.to_string ty) | ty -> unsupported "slice of %s" (Types.to_string ty)
in in

View File

@ -2074,7 +2074,7 @@ static void crash_handler(int sig, siginfo_t *si, void *uc) {
} }
{ {
static const char why[] = static const char why[] =
"\nflan: a write through a read-only slice (bytes-view of a literal), " "\nflan: a write through a pointer into read-only memory, "
"a null, or a stack overflow\n"; "a null, or a stack overflow\n";
crash_puts(why, sizeof why - 1); crash_puts(why, sizeof why - 1);
} }

View File

@ -13,7 +13,7 @@
(defn eight [a i64 b i64 c i64 d i64 e i64 f i64 g i64 h i64] i64 (defn eight [a i64 b i64 c i64 d i64 e i64 f i64 g i64 h i64] i64
(+ (+ (+ a b) (+ c d)) (+ (+ e f) (+ g h)))) (+ (+ (+ a b) (+ c d)) (+ (+ e f) (+ g h))))
(defn taglen [s [u8]] i64 (defn taglen [s [const u8]] i64
(i64 (length s))) (i64 (length s)))
(defn main [] i32 (defn main [] i32

View File

@ -94,30 +94,15 @@ forever="dev-loop dev-watch dev-chatty agent-auto"
# in the epilogue that every exit already went through. The five are in the # in the epilogue that every exit already went through. The five are in the
# sweep now and they are five of the MATCHes. # sweep now and they are five of the MATCHes.
# The ones whose whole point is a fault, and which therefore cannot be compared # The one whose whole point is a fault, and which therefore cannot be compared
# at this sweep's optimisation level. Both write through a bytes-view of a # at this sweep's optimisation level. dev-segv stores through a pointer to a
# string literal, which is a store into .rodata: measured here, LLVM exits 0 # string literal's bytes, which is a store into .rodata: LLVM at -O2 deletes it
# having printed the unmodified literal and this backend exits 139, because the # as undefined and exits 0, and this backend has no optimiser and exits 139.
# store is undefined and the optimiser deleted it on one side and there is no # That is not a lowering disagreement. test_dev.ml builds it in a dev session,
# optimiser on the other. That is not a lowering disagreement. At -O0 the two # where the fault is the thing asserted. It also calls agent/start, so it
# agree exactly -- 139, no output, both backends -- and test_acceptance.ml's
# dies_segv rows pin precisely that, on both backends, which is the coverage
# this sweep would otherwise be duplicating at the one level where, as that
# file's own comment puts it, there is nothing left to pin but the UB.
#
# Excluded by name rather than by building these two at -O0 here, and the
# reason is the coverage and not the counts: a per-name -O0 list would move the
# counts exactly as much as this does, so that is no argument at all. The
# argument is that dies_segv already builds both of them at -O0, on both
# backends, and asserts the exit status and the empty output -- everything this
# sweep would check, in the file where the ruling is written down.
#
# One more thing about dev-segv, which is a reason to keep it out of the
# comparison rather than a reason for this list: it calls agent/start, so it
# leaves a socket in /tmp on both runs, and under SURVEY_FLAGS=--dev it parks # leaves a socket in /tmp on both runs, and under SURVEY_FLAGS=--dev it parks
# in the break loop instead of dying -- which is a forever-list problem, met # in the break loop instead of dying.
# here by a program that was never going to be compared anyway. faults="dev-segv"
faults="bytes-view-write dev-segv"
TIMEOUT=${TIMEOUT:-20} TIMEOUT=${TIMEOUT:-20}

View File

@ -13,7 +13,7 @@
(print " ")) (print " "))
(println "")) (println ""))
(defn show-fields [s [[u8]]] () (defn show-fields [s [[const u8]]] ()
(dotimes [i (length s)] (dotimes [i (length s)]
(print (string (at s i))) (print (string (at s i)))
(print " ")) (print " "))

View File

@ -85,7 +85,7 @@
(set frames (+ frames 1))) (set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1))))) (continue [] (set skipped (+ skipped 1)))))
(defn slice-frame [s [u8] lo i32 hi i32] () (defn slice-frame [s [const u8] lo i32 hi i32] ()
(restart-case (restart-case
(do (show "slice" (i64 (length (slice s lo hi)))) (do (show "slice" (i64 (length (slice s lo hi))))
(set frames (+ frames 1))) (set frames (+ frames 1)))

View File

@ -1,19 +1,11 @@
;;;; A store through (bytes-view "literal") lands in the string constant's ;;;; A store through (bytes-view "literal") is refused at compile time:
;;;; own storage, which both backends emit read-only — LLVM as a `constant` ;;;; bytes-view answers a [const u8], because a string literal's bytes are in
;;;; global, x86 in .rodata — so the write traps where it happens instead of ;;;; read-only memory, where the store would trap at -O0 and be deleted as
;;;; corrupting the literal. Pinned at -O0 on both backends, where the store ;;;; undefined at -O2. test_acceptance.ml asserts the refusal; nothing here is
;;;; is really emitted; at -O2 LLVM deletes it as undefined behaviour, which ;;;; ever built.
;;;; is why this program has no -O2 row. The trap itself (SIGSEGV on a
;;;; read-only page) is the defined consequence of the emission, not a bet on
;;;; anything further.
;;;;
;;;; If this ever exits 0, string data has become writable somewhere and the
;;;; read-only-by-convention story of bytes-view is silently gone.
(defn main [] i32 (defn main [] i32
(let [v (bytes-view "INSERTIONSORT")] (let [v (bytes-view "INSERTIONSORT")]
(set (at v 0) \Z) (set (at v 0) \Z)
;; Never reached: the store above traps. Printing anyway makes a failure
;; loud — output where none was expected.
(print (string v)) (print (string v))
0)) 0))

View File

@ -0,0 +1,49 @@
;;;; [const T]: a slice that can only be read. bytes-view answers one, a [T]
;;;; converts to one wherever one is wanted, and slicing one keeps it
;;;; read-only. Nothing about it exists at run time, so this prints the same
;;;; on every backend and at every level.
(defn total [s [const i32]] i64
(let [t (i64 0)]
(dotimes [i (length s)]
(set t (+ t (at s i))))
t))
;; A generic over a read-only slice takes a writable one too.
(defn first-of [s [const $t]] $t (at s 0))
(defn widths [parts [const [const u8]]] i32
(let [n 0]
(dotimes [i (length parts)]
(set n (+ n (length (at parts i)))))
n))
(defn main [] i32
(let [xs [3 1 2]
w (slice xs)
r (bytes-view "hello, world")
head (slice r 0 5)
tail (slice r 7)
names (vec-new [const u8])]
(sort w)
(println (total w) (total (slice w 1)))
(println (first-of w) (first-of (bytes-view "z")))
(println (string head) (string tail) (length head))
(println (bytes=? head (bytes-view "hello")) (starts-with? r head))
(push names head)
(push names tail)
(println (widths (slice names)))
;; A writable [[u8]] meets [const [const u8]] too: the outer view is
;; read-only, so nothing can put a read-only slice into it.
(let [a (bytes "ab")
b (bytes "cde")
both [a b]]
(println (widths (slice both)))
(set (at a 0) \A)
(println (string a)))
(let [f (split (bytes-view "b,a,c") \,)]
(sort-bytes (slice f))
(println (string (slice (join (slice f) (bytes-view "-"))))))
(println (at r 0))
(free names))
0)

View File

@ -1,17 +1,22 @@
;;;; The dogfooding crash, replayed on purpose: a write through a bytes-view ;;;; The dogfooding crash, replayed on purpose: a store through a pointer to
;;;; of a string literal lands in read-only memory and takes SIGSEGV. In a ;;;; a string literal's bytes lands in read-only memory and takes SIGSEGV. In
;;;; dev session that used to kill the whole process — daemon, compiler and ;;;; a dev session that used to kill the whole process — daemon, compiler and
;;;; socket together, with no message at all. The dev build's crash handler ;;;; socket together, with no message at all. The dev build's crash handler
;;;; turns it into the same park the no-channel traps take: one line naming ;;;; turns it into the same park the no-channel traps take: one line naming
;;;; the address and the frame, then the break loop, with the daemon alive ;;;; the address and the frame, then the break loop, with the daemon alive
;;;; and answering behind it. There is no restart to list — a faulting ;;;; and answering behind it. There is no restart to list — a faulting
;;;; instruction has nowhere to resume at — which is the same empty-list ;;;; instruction has nowhere to resume at — which is the same empty-list
;;;; shape dev-trap-null-alloc.flan pins for free-all. ;;;; shape dev-trap-null-alloc.flan pins for free-all.
;;;;
;;;; Through a pointer, because a store through the [const u8] itself is
;;;; refused at compile time; a (Ptr T) is the C boundary, where nothing is
;;;; checked.
(import agent "vendor:agent") (import agent "vendor:agent")
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-segv-fallback.sock") (agent/start "/tmp/flan-dev-segv-fallback.sock")
(let [v (bytes-view "INSERTIONSORT")] (let [v (bytes-view "INSERTIONSORT")
(set (at v 0) \Z) p (addr (at v 0))]
(set (deref p) \Z)
(print (string v)) (print (string v))
0)) 0))

View File

@ -70,11 +70,11 @@
;; ── The worked example: a struct read by hand ─────────────────────── ;; ── The worked example: a struct read by hand ───────────────────────
;; `name` is a [u8] and not a copy of one, so an Enemy is only valid while the ;; `name` is a [const u8] and not a copy of one, so an Enemy is only valid while the
;; buffer it was read out of is. That is the lifetime contract from the package ;; buffer it was read out of is. That is the lifetime contract from the package
;; header, and it is what a struct reader inherits by using slices. ;; header, and it is what a struct reader inherits by using slices.
(defstruct Enemy (defstruct Enemy
[name [u8] [name [const u8]
hp i32 hp i32
speed f32 speed f32
boss? bool]) boss? bool])

View File

@ -13,7 +13,7 @@
(defstruct Local [n i32]) (defstruct Local [n i32])
;;; The case that used to fail: a package struct as the declared return type. ;;; The case that used to fail: a package struct as the declared return type.
(defn fresh [src [u8]] edn/Cursor (edn/cursor src)) (defn fresh [src [const u8]] edn/Cursor (edn/cursor src))
;;; And the one that must keep working: a lowercase qualified name in the same ;;; And the one that must keep working: a lowercase qualified name in the same
;;; position is an expression, not a type. ;;; position is an expression, not a type.

View File

@ -50,7 +50,7 @@
;; code/width/ok, so a wrong answer names which of the three it got wrong ;; code/width/ok, so a wrong answer names which of the three it got wrong
;; rather than just failing. ;; rather than just failing.
(defn show-dec [s [u8]] () (defn show-dec [s [const u8]] ()
(let [r (decode-rune s)] (let [r (decode-rune s)]
(print (.code r)) (print "/") (print (.code r)) (print "/")
(print (.width r)) (print "/") (print (.width r)) (print "/")
@ -78,7 +78,7 @@
(print x) (print x)
(print " ")) (print " "))
(defn show-split [s [u8] sep u8] () (defn show-split [s [const u8] sep u8] ()
(let [it (split-on-byte s sep) (let [it (split-on-byte s sep)
going true] going true]
(while going (while going

View File

@ -6,7 +6,7 @@
(defn main [] i32 (defn main [] i32
(let [x 7 (let [x 7
v (builtin/vec-new [u8])] v (builtin/vec-new [const u8])]
(println (vec-new [x])) (println (vec-new [x]))
(push v (bytes-view "ab")) (push v (bytes-view "ab"))
(println (length (at v 0))) (println (length (at v 0)))

View File

@ -5,11 +5,11 @@
(defn main [] i32 (defn main [] i32
(let [a (arena-new 4096) (let [a (arena-new 4096)
words (vec-new [u8]) words (vec-new [const u8])
pairs (vec-new [2 i32]) pairs (vec-new [2 i32])
ptrs (vec-new (Ptr i32)) ptrs (vec-new (Ptr i32))
opts (vec-new (Option i64) a) opts (vec-new (Option i64) a)
m (map-new string [u8]) m (map-new string [const u8])
x (i32 7)] x (i32 7)]
(push words (bytes-view "ab")) (push words (bytes-view "ab"))
(push words (bytes-view "cde")) (push words (bytes-view "cde"))

View File

@ -840,26 +840,33 @@ let () =
"programs/bytes-copy.flan" bytes_copy_out; "programs/bytes-copy.flan" bytes_copy_out;
outputs ~x86:true "bytes copies, bytes-view aliases, --x86" outputs ~x86:true "bytes copies, bytes-view aliases, --x86"
"programs/bytes-copy.flan" bytes_copy_out; "programs/bytes-copy.flan" bytes_copy_out;
(* The other half of the same ruling: a store through a bytes-view of a (* The other half of the same ruling: a store through a bytes-view is
literal traps, identically on both backends, because both emit string refused before anything is built, because bytes-view answers a
data read-only. -O0 only — at -O2 LLVM deletes the store as UB, so [const u8]. It used to compile and trap at -O0 on both backends, and be
there is nothing there to pin except the UB itself. 139 is the shell's deleted as undefined at -O2. *)
128+SIGSEGV. *) (let path = "programs/bytes-view-write.flan" in
let dies_segv name path ~x86 = match
let exe = compile ~opt:"-O0" ~x86 path in Check.program (Load.program ~file:path (Reader.read_file path)).Load.decls
let code, text = run exe None in with
if code <> 139 || text <> "" then begin | _ ->
incr failures; incr failures;
Printf.printf Printf.printf "FAIL a write through bytes-view is refused\n"
"FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 139)\n" | exception Loc.Error { Loc.dmsg = m; _ } ->
name text code "" if not (contains m "this writes through a [const u8]") then begin
end; incr failures;
(try Sys.remove exe with Sys_error _ -> ()) Printf.printf
"FAIL a write through bytes-view is refused\n said: %S\n" m
end);
(* [const T]: the checker's alone, so the three builds agree and every
row is about which values reach which parameters. *)
let const_slice_out =
"6 5\n1 122\nhello world 5\ntrue true\n10\n5\nAb\na-b-c\n104\n"
in in
dies_segv "a write through bytes-view traps, -O0" outputs "const slices" "programs/const-slice.flan" const_slice_out;
"programs/bytes-view-write.flan" ~x86:false; outputs ~opt:"-O0" "const slices, -O0" "programs/const-slice.flan"
dies_segv "a write through bytes-view traps, --x86" const_slice_out;
"programs/bytes-view-write.flan" ~x86:true; outputs ~x86:true "const slices, --x86" "programs/const-slice.flan"
const_slice_out;
(* (string b). The conversion emits nothing — String and Slice _ are the (* (string b). The conversion emits nothing — String and Slice _ are the
same %slice — so the rows are about length and ownership rather than same %slice — so the rows are about length and ownership rather than
arithmetic: a number round-tripped, an empty slice, sub-views whose arithmetic: a number round-tripped, an empty slice, sub-views whose

View File

@ -442,7 +442,7 @@ let () =
[Rune] also pins the spelling — the types read exactly as [defs] [Rune] also pins the spelling — the types read exactly as [defs]
spells a signature, because both go through [Types.to_string]. *) spells a signature, because both go through [Types.to_string]. *)
let r = request c "(:op \"layout\" :type \"Split\")" in let r = request c "(:op \"layout\" :type \"Split\")" in
if fields r <> [ "rest [u8]"; "sep u8"; "more bool" ] then if fields r <> [ "rest [const u8]"; "sep u8"; "more bool" ] then
fail "Split's fields: %s" (String.concat ", " (fields r)); fail "Split's fields: %s" (String.concat ", " (fields r));
let r = request c "(:op \"layout\" :type \"Nonesuch\")" in let r = request c "(:op \"layout\" :type \"Nonesuch\")" in
@ -1922,7 +1922,7 @@ let () =
if refault then begin if refault then begin
let faulting = let faulting =
"(:op \"eval-expr\" :code \ "(:op \"eval-expr\" :code \
\"(let [v (bytes-view \\\"refault\\\")] (set (at v 0) 90) 1)\" \ \"(let [v (bytes-view \\\"refault\\\") p (addr (at v 0))] (set (deref p) 90) 1)\" \
:file \"/tmp/buf.flan\")" :file \"/tmp/buf.flan\")"
in in
(match ask faulting with _ -> () | exception _ -> ()); (match ask faulting with _ -> () | exception _ -> ());
@ -2028,8 +2028,9 @@ let () =
word. The dev build's crash handler (flan_dev_crash_enable) enters the word. The dev build's crash handler (flan_dev_crash_enable) enters the
same trap hook the six no-channel refusals use, so everything trap_park same trap hook the six no-channel refusals use, so everything trap_park
asserts for them holds here too: stopped and describable, an eval still asserts for them holds here too: stopped and describable, an eval still
answered, a resume refused. The program writes through bytes-view, answered, a resume refused. The program writes through a pointer to
which is the surviving spelling of that crash. *) a literal's bytes, which is the surviving spelling of that crash: a
store through the bytes-view itself no longer compiles. *)
trap_park ~refault:true "segfault" "dev-segv.flan" "SegFault" []; trap_park ~refault:true "segfault" "dev-segv.flan" "SegFault" [];
(* ── The locals of a stopped frame ─────────────────────────────── *) (* ── The locals of a stopped frame ─────────────────────────────── *)

View File

@ -468,7 +468,23 @@ let () =
| { d = Defn { praw = Some [ Pname ("x", _); Ptype t ]; _ }; _ } -> t.t | { d = Defn { praw = Some [ Pname ("x", _); Ptype t ]; _ }; _ } -> t.t
| _ -> failwith "bad type test" | _ -> failwith "bad type test"
in in
(match ty "[u8]" with Tslice _ -> () | _ -> check "[T] is a slice" false); (match ty "[u8]" with
| Tslice (false, _) -> () | _ -> check "[T] is a slice" false);
(* [const] is reserved, so this is never [n T] with a length named const. *)
(match ty "[const u8]" with
| Tslice (true, { t = Tname "u8"; _ }) -> ()
| _ -> check "[const T] is a read-only slice" false);
(match ty "[const [const u8]]" with
| Tslice (true, { t = Tslice (true, _); _ }) -> ()
| _ -> check "[const [const T]] nests" false);
(match ty "[const]" with
| exception Loc.Error { Loc.dmsg; _ }
when contains dmsg "[const] names no element type" -> ()
| _ -> check "[const] alone is refused" false);
(match ty "[const 4 u8]" with
| exception Loc.Error { Loc.dmsg; _ }
when contains dmsg "has no read-only form" -> ()
| _ -> check "[const 4 u8] is refused" false);
(match ty "[4 f32]" with (match ty "[4 f32]" with
| Tarray (Lint 4L, _) -> () | _ -> check "[n T] is an array" false); | Tarray (Lint 4L, _) -> () | _ -> check "[n T] is an array" false);
(match ty "[rows [cols u32]]" with (match ty "[rows [cols u32]]" with
@ -568,7 +584,7 @@ let () =
(match (parse_decl "(defmacro m [& args] (at args 0))").d with (match (parse_decl "(defmacro m [& args] (at args 0))").d with
| Defn { name = "m"; params = [ p ]; ret = Some r; _ } -> | Defn { name = "m"; params = [ p ]; ret = Some r; _ } ->
(match p.fty.t, r.t with (match p.fty.t, r.t with
| Tslice { t = Tname "Form"; _ }, Tname "Form" -> () | Tslice (false, { t = Tname "Form"; _ }), Tname "Form" -> ()
| _ -> check "defmacro is [Form] -> Form" false) | _ -> check "defmacro is [Form] -> Form" false)
| _ -> check "defmacro parses as a defn" false); | _ -> check "defmacro parses as a defn" false);
@ -578,7 +594,7 @@ let () =
(match (parse_decl "(defmacro m [[a b] c & rest] (at rest 0))").d with (match (parse_decl "(defmacro m [[a b] c & rest] (at rest 0))").d with
| Defn { name = "m"; params = [ p ]; ret = Some r; _ } -> | Defn { name = "m"; params = [ p ]; ret = Some r; _ } ->
(match p.fty.t, r.t with (match p.fty.t, r.t with
| Tslice { t = Tname "Form"; _ }, Tname "Form" -> () | Tslice (false, { t = Tname "Form"; _ }), Tname "Form" -> ()
| _ -> check "a parameter list is still [Form] -> Form" false) | _ -> check "a parameter list is still [Form] -> Form" false)
| _ -> check "a macro with a parameter list parses as a defn" false); | _ -> check "a macro with a parameter list parses as a defn" false);
@ -1052,7 +1068,7 @@ let () =
runs no passes over it. *) runs no passes over it. *)
infers "array-fill of nothing" "(array-fill [0] 1)" "[0 i32]"; infers "array-fill of nothing" "(array-fill [0] 1)" "[0 i32]";
infers "bytes of a string" "(bytes \"hi\")" "[u8]"; infers "bytes of a string" "(bytes \"hi\")" "[u8]";
infers "bytes-view of a string" "(bytes-view \"hi\")" "[u8]"; infers "bytes-view of a string" "(bytes-view \"hi\")" "[const u8]";
infers "length is i32" "(length (bytes \"hi\"))" "i32"; infers "length is i32" "(length (bytes \"hi\"))" "i32";
infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]"; infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]";
infers "slice of the whole" "(slice (bytes \"hi\"))" "[u8]"; infers "slice of the whole" "(slice (bytes \"hi\"))" "[u8]";
@ -2197,6 +2213,63 @@ let () =
"(defn g [b [u8]] i32 (length b)) (defn f [s string] i32 (g (slice s)))" "(defn g [b [u8]] i32 (length b)) (defn f [s string] i32 (g (slice s)))"
~needle:"expected [u8], found string"; ~needle:"expected [u8], found string";
(* [const T]: a view that can only be read. Every route to a store through
one is refused, and none of the reads is. *)
infers "a const slice slices to a const slice"
"(slice (bytes-view \"hello\") 1 3)" "[const u8]";
infers "vec-new reads [const u8] as a type" "(vec-new [const u8])"
"(Vec [const u8])";
infers "map-new reads [const u8] as a type" "(map-new string [const u8])"
"(Map string [const u8])";
rejects_check "set through a const slice"
"(defn f [s [const u8]] () (set (at s 0) 65))"
~needle:"this writes through a [const u8]";
rejects_check "set through bytes-view"
"(defn f [] () (let [v (bytes-view \"Hi\")] (set (at v 0) \\h)))"
~needle:"this writes through a [const u8]";
rejects_check "set through a const slice, two indices"
"(defn f [s [const [2 i32]]] () (set (at s 0 1) 5))"
~needle:"this writes through a [const [2 i32]]";
rejects_check "set through an array element of a const slice"
"(defn f [s [const [2 i32]]] () (set (at (at s 0) 1) 5))"
~needle:"this writes through a [const [2 i32]]";
rejects_check "replace an array element of a const slice whole"
"(defn f [s [const [2 i32]]] () (set (at s 0) [1 2]))"
~needle:"this writes through a [const [2 i32]]";
rejects_check "set a field of a const slice's element"
"(defstruct P [x i32]) (defn f [s [const P]] () (set (.x (at s 0)) 5))"
~needle:"this writes through a [const P]";
rejects_check "a const slice is not a writable one"
"(defn g [b [u8]] () (set (at b 0) 1)) (defn f [s [const u8]] () (g s))"
~needle:"expected [u8], found [const u8]";
rejects_check "and the refusal names the copy"
"(defn g [b [u8]] () (set (at b 0) 1)) (defn f [s [const u8]] () (g s))"
~needle:"(bytes (string v)) copies v";
rejects_check "a generic writer does not take a const slice"
"(defn f [s [const i32]] () (sort s))"
~needle:"sort takes a slice it may write through";
rejects_check "a const slice does not cross into dyn"
"(defn f [s [const i64]] dyn s)"
~needle:"a [const i64] can only be read";
rejects_check "no conversion under a writable slice"
"(defn g [p [[const u8]]] i32 0) (defn f [p [[u8]]] i32 (g p))"
~needle:"expected [[const u8]], found [[u8]]";
rejects_check "const is not a name a constant can have"
"(defconst const 4)" ~needle:"const cannot be declared";
(* The const is shallow: an element of a [const [u8]] is a writable [u8]. *)
accepts "store through an element of a const slice of slices"
"(defn f [s [const [u8]]] () (set (at (at s 0) 1) 5))";
accepts "a writable slice is a const one"
"(defn g [b [const u8]] u8 (at b 0)) (defn f [s [u8]] u8 (g s))";
accepts "and so is a string's bytes, at a prelude reader"
"(defn f [s string] bool (bytes=? (bytes-view s) (bytes-view \"x\")))";
accepts "under a const slice the element converts too"
"(defn g [p [const [const u8]]] i32 0) (defn f [p [[u8]]] i32 (g p))";
accepts "the address of a const element, for C"
"(defn f [s [const u8]] (Ptr u8) (addr (at s 0)))";
accepts "a generic reader takes both"
"(defn f [a [const i32] b [i32]] i64 (+ (sum-i32 a) (sum-i32 b)))";
(* (slice-from-ptr p n). The one form in the language whose central claim the (* (slice-from-ptr p n). The one form in the language whose central claim the
compiler cannot check — whether n is the truth about what p addresses — so compiler cannot check — whether n is the truth about what p addresses — so
what it does check is worth pinning: the argument really is a pointer, the what it does check is worth pinning: the argument really is a pointer, the

View File

@ -440,7 +440,7 @@ let dev_sweep () =
handler's line, and that is the assertion. handler's line, and that is the assertion.
And it has to be built at -O0. At the sweep's -O2 the write through a And it has to be built at -O0. At the sweep's -O2 the write through a
bytes-view of a string literal does not fault at all — measured, both pointer to a string literal's bytes does not fault at all — measured, both
builds print the unmodified string — so a case that is about what happens builds print the unmodified string — so a case that is about what happens
on a fault has to be compiled where the fault happens. Same family as the on a fault has to be compiled where the fault happens. Same family as the
-O0/-O2 split [unchecked_controls] records for bounds.flan. -O0/-O2 split [unchecked_controls] records for bounds.flan.

10
vendor/edn/edn.flan vendored
View File

@ -1,4 +1,4 @@
;;;; An EDN tokenizer, in Flan, over a [u8]. ;;;; An EDN tokenizer, in Flan, over a [const u8].
;;;; ;;;;
;;;; This is the bottom layer of a reader. It answers one question — "what is ;;;; This is the bottom layer of a reader. It answers one question — "what is
;;;; the next token, and where" — and it answers it without allocating ;;;; the next token, and where" — and it answers it without allocating
@ -161,7 +161,7 @@
;; usable underline position even though `text` is narrower than the token. ;; usable underline position even though `text` is narrower than the token.
(defstruct Token (defstruct Token
[kind i32 [kind i32
text [u8] text [const u8]
pos i32]) pos i32])
;; The cursor owns no storage either: `src` is the caller's buffer. ;; The cursor owns no storage either: `src` is the caller's buffer.
@ -171,7 +171,7 @@
;; left to a parser, because `[1 2}` is malformed in a way only the tokenizer ;; left to a parser, because `[1 2}` is malformed in a way only the tokenizer
;; has the position for. ;; has the position for.
(defstruct Cursor (defstruct Cursor
[src [u8] [src [const u8]
pos i32 pos i32
err i32 err i32
err-pos i32 err-pos i32
@ -180,7 +180,7 @@
;; ── Construction ──────────────────────────────────────────────────── ;; ── Construction ────────────────────────────────────────────────────
(defn cursor [src [u8]] Cursor (defn cursor [src [const u8]] Cursor
(Cursor {.src src .pos 0 .err err-none .err-pos 0 .depth 0})) (Cursor {.src src .pos 0 .err err-none .err-pos 0 .depth 0}))
(defn ok? [c (Ptr Cursor)] bool (defn ok? [c (Ptr Cursor)] bool
@ -266,7 +266,7 @@
;; text of their own — eof, error, and every delimiter. It is still a slice of ;; text of their own — eof, error, and every delimiter. It is still a slice of
;; the input rather than a slice of nothing, so `text` has one meaning for all ;; the input rather than a slice of nothing, so `text` has one meaning for all
;; token kinds. ;; token kinds.
(defn- empty-at [c (Ptr Cursor) p i32] [u8] (defn- empty-at [c (Ptr Cursor) p i32] [const u8]
(slice (.src c) p p)) (slice (.src c) p p))
(defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token (defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token

View File

@ -80,7 +80,7 @@
;; buffer costs nothing to produce. A person reading a refusal wants a line and ;; buffer costs nothing to produce. A person reading a refusal wants a line and
;; a column, so the newlines before the offset are counted here — once per ;; a column, so the newlines before the offset are counted here — once per
;; refusal, which is as often as this is ever called. ;; refusal, which is as often as this is ever called.
(defn- where [src [u8] pos i32] string (defn- where [src [const u8] pos i32] string
(let [line (i64 1) (let [line (i64 1)
col (i64 1) col (i64 1)
i (i32 0)] i (i32 0)]
@ -212,7 +212,7 @@
;; One value, from the cursor's current position, consumed. `name` is what a ;; One value, from the cursor's current position, consumed. `name` is what a
;; struct here would be called; `src` is the whole buffer, for the positions a ;; struct here would be called; `src` is the whole buffer, for the positions a
;; refusal names. ;; refusal names.
(defn- derive [c (Ptr Cursor) name string src [u8]] Derived (defn- derive [c (Ptr Cursor) name string src [const u8]] Derived
(let [t (next c)] (let [t (next c)]
(when (not (ok? c)) (when (not (ok? c))
(return (derived-bad (return (derived-bad
@ -242,7 +242,7 @@
;; decides; every one after it is compared against that decision and both ;; decides; every one after it is compared against that decision and both
;; positions are named when they disagree, because "heterogeneous" without ;; positions are named when they disagree, because "heterogeneous" without
;; saying where sends someone to read the whole file. ;; saying where sends someone to read the whole file.
(defn- derive-vec [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (defn- derive-vec [c (Ptr Cursor) name string at-pos i32 src [const u8]] Derived
(when (at-byte? c \]) (when (at-byte? c \])
(return (derived-bad (return (derived-bad
(joined3 "the empty vector at " (where src at-pos) (joined3 "the empty vector at " (where src at-pos)
@ -291,7 +291,7 @@
;; A set becomes `(Map T bool)`, so its elements are map keys. `derive-key` is ;; A set becomes `(Map T bool)`, so its elements are map keys. `derive-key` is
;; where that constraint is enforced and said. ;; where that constraint is enforced and said.
(defn- derive-set [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (defn- derive-set [c (Ptr Cursor) name string at-pos i32 src [const u8]] Derived
(when (at-byte? c \}) (when (at-byte? c \})
(return (derived-bad (return (derived-bad
(joined3 "the empty set at " (where src at-pos) (joined3 "the empty set at " (where src at-pos)
@ -326,7 +326,7 @@
;; fixed array, which is one where a Vec is not; anything else is refused here ;; fixed array, which is one where a Vec is not; anything else is refused here
;; rather than at the `(Map ...)` the caller would build out of it, because a ;; rather than at the `(Map ...)` the caller would build out of it, because a
;; map-key refusal names a type nobody wrote. ;; map-key refusal names a type nobody wrote.
(defn- derive-key [c (Ptr Cursor) name string src [u8]] Derived (defn- derive-key [c (Ptr Cursor) name string src [const u8]] Derived
(when (at-byte? c \[) (when (at-byte? c \[)
(return (derive-array c name src))) (return (derive-array c name src)))
(let [d (derive c name src)] (let [d (derive c name src)]
@ -342,7 +342,7 @@
;; of the set has to be the same length as well as the same shape — which falls ;; of the set has to be the same length as well as the same shape — which falls
;; out of the type comparison the caller already makes, since the length is in ;; out of the type comparison the caller already makes, since the length is in
;; the type it compares. ;; the type it compares.
(defn- derive-array [c (Ptr Cursor) name string src [u8]] Derived (defn- derive-array [c (Ptr Cursor) name string src [const u8]] Derived
(let [open (next c)] (let [open (next c)]
(when (at-byte? c \]) (when (at-byte? c \])
(return (derived-bad (return (derived-bad
@ -379,7 +379,7 @@
(expect c tok-vec-close) (expect c tok-vec-close)
arr))))))) arr)))))))
(defn- disagreement [what string src [u8] at-pos i32 n i64 (defn- disagreement [what string src [const u8] at-pos i32 n i64
first Form second Form] string first Form second Form] string
(joined3 (joined3 "the " what " at ") (joined3 (joined3 "the " what " at ")
(where src at-pos) (where src at-pos)
@ -400,7 +400,7 @@
;; differently is the two arms a hand-written reader had no reason to have — a ;; differently is the two arms a hand-written reader had no reason to have — a
;; key that is not a field of the struct, and a field the file did not have. ;; key that is not a field of the struct, and a field the file did not have.
;; Both signal SchemaDrift. See the note over that type. ;; Both signal SchemaDrift. See the note over that type.
(defn- derive-map [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (defn- derive-map [c (Ptr Cursor) name string at-pos i32 src [const u8]] Derived
(when (at-byte? c \}) (when (at-byte? c \})
(return (derived-bad (return (derived-bad
(joined3 "the empty map at " (where src at-pos) (joined3 "the empty map at " (where src at-pos)
@ -543,7 +543,7 @@
_ (refuse "defedn's first argument is the name of the struct to declare, written as a name")) _ (refuse "defedn's first argument is the name of the struct to declare, written as a name"))
_ (refuse "defedn's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) _ (refuse "defedn's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from"))))
(defn- provide [name string path string src [u8]] Form (defn- provide [name string path string src [const u8]] Form
(let [cur (cursor src) (let [cur (cursor src)
d (derive (addr cur) name src)] d (derive (addr cur) name src)]
(if (bad? d) (if (bad? d)
@ -560,7 +560,7 @@
;; fields are built in, because a string field is a copy and a Vec ;; fields are built in, because a string field is a copy and a Vec
;; field is an allocation — spec-memory's rule, and the reason the ;; field is an allocation — spec-memory's rule, and the reason the
;; destination is never implicit. ;; destination is never implicit.
(defn ~bname [b [u8] a Allocator] ~sname (defn ~bname [b [const u8] a Allocator] ~sname
(let [c (cursor b) (let [c (cursor b)
out (~rname (addr c) a)] out (~rname (addr c) a)]
;; The cursor is made and dropped here, so this is the only ;; The cursor is made and dropped here, so this is the only

View File

@ -65,7 +65,7 @@
;; dropped here on purpose. Nothing individually owns a block in a region — ;; dropped here on purpose. Nothing individually owns a block in a region —
;; free-all owns all of them — so keeping the header around to free through ;; free-all owns all of them — so keeping the header around to free through
;; would be keeping a handle for an operation that never happens. ;; would be keeping a handle for an operation that never happens.
(defn copy-text [s [u8]] string (defn copy-text [s [const u8]] string
(let [b (vec-new u8)] (let [b (vec-new u8)]
(append (addr b) s) (append (addr b) s)
(string (slice b)))) (string (slice b))))
@ -136,7 +136,7 @@
;; The whole document, from a byte slice. nil when the input was malformed — ;; The whole document, from a byte slice. nil when the input was malformed —
;; the header says what that conflates and what to do when it matters. ;; the header says what that conflates and what to do when it matters.
(defn read [src [u8]] dyn (defn read [src [const u8]] dyn
(let [c (cursor src) (let [c (cursor src)
t (next (addr c)) t (next (addr c))
v (read-value (addr c) t)] v (read-value (addr c) t)]

10
vendor/json/json.flan vendored
View File

@ -1,4 +1,4 @@
;;;; A JSON tokenizer, in Flan, over a [u8]. ;;;; A JSON tokenizer, in Flan, over a [const u8].
;;;; ;;;;
;;;; The shape is vendor/edn's, deliberately: a Cursor over a caller's buffer, ;;;; The shape is vendor/edn's, deliberately: a Cursor over a caller's buffer,
;;;; one `next` that answers a Token, errors accumulated on the cursor with the ;;;; one `next` that answers a Token, errors accumulated on the cursor with the
@ -205,7 +205,7 @@
;; underline position even where `text` is narrower than the token. ;; underline position even where `text` is narrower than the token.
(defstruct Token (defstruct Token
[kind i32 [kind i32
text [u8] text [const u8]
pos i32]) pos i32])
;; The cursor owns no storage: `src` is the caller's buffer. ;; The cursor owns no storage: `src` is the caller's buffer.
@ -214,7 +214,7 @@
;; each one waits for, so that {"a": [1} fails at the brace with a position ;; each one waits for, so that {"a": [1} fails at the brace with a position
;; rather than confusing a reader two levels up. ;; rather than confusing a reader two levels up.
(defstruct Cursor (defstruct Cursor
[src [u8] [src [const u8]
pos i32 pos i32
err i32 err i32
err-pos i32 err-pos i32
@ -223,7 +223,7 @@
;; ── Construction ──────────────────────────────────────────────────── ;; ── Construction ────────────────────────────────────────────────────
(defn cursor [src [u8]] Cursor (defn cursor [src [const u8]] Cursor
(Cursor {.src src .pos 0 .err err-none .err-pos 0 .depth 0})) (Cursor {.src src .pos 0 .err err-none .err-pos 0 .depth 0}))
(defn ok? [c (Ptr Cursor)] bool (defn ok? [c (Ptr Cursor)] bool
@ -313,7 +313,7 @@
;; An empty slice of src, positioned at p. Used for the tokens that have no ;; An empty slice of src, positioned at p. Used for the tokens that have no
;; text of their own — eof, error, and every delimiter — so that `text` has one ;; text of their own — eof, error, and every delimiter — so that `text` has one
;; meaning for every token kind and not two. ;; meaning for every token kind and not two.
(defn- empty-at [c (Ptr Cursor) p i32] [u8] (defn- empty-at [c (Ptr Cursor) p i32] [const u8]
(slice (.src c) p p)) (slice (.src c) p p))
(defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token (defn- token [c (Ptr Cursor) kind i32 lo i32 hi i32 p i32] Token

View File

@ -63,7 +63,7 @@
;; The tokenizer answers byte offsets. A person reading a refusal wants a line ;; The tokenizer answers byte offsets. A person reading a refusal wants a line
;; and a column, so the newlines before the offset are counted here — once per ;; and a column, so the newlines before the offset are counted here — once per
;; refusal, which is as often as this is ever called. ;; refusal, which is as often as this is ever called.
(defn- where [src [u8] pos i32] string (defn- where [src [const u8] pos i32] string
(let [line (i64 1) (let [line (i64 1)
col (i64 1) col (i64 1)
i (i32 0)] i (i32 0)]
@ -173,7 +173,7 @@
;; ── Deriving ──────────────────────────────────────────────────────── ;; ── Deriving ────────────────────────────────────────────────────────
(defn- derive [c (Ptr Cursor) name string src [u8]] Derived (defn- derive [c (Ptr Cursor) name string src [const u8]] Derived
(let [t (next c)] (let [t (next c)]
(when (not (ok? c)) (when (not (ok? c))
(return (derived-bad (return (derived-bad
@ -202,7 +202,7 @@
;; every one after it is compared against that, and both positions are named ;; every one after it is compared against that, and both positions are named
;; when they disagree — "heterogeneous" on its own sends someone to read the ;; when they disagree — "heterogeneous" on its own sends someone to read the
;; whole file. ;; whole file.
(defn- derive-array [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (defn- derive-array [c (Ptr Cursor) name string at-pos i32 src [const u8]] Derived
(when (at-byte? c \]) (when (at-byte? c \])
(return (derived-bad (return (derived-bad
(joined3 "the empty array at " (where src at-pos) (joined3 "the empty array at " (where src at-pos)
@ -248,7 +248,7 @@
;; ── An object, which is a struct ──────────────────────────────────── ;; ── An object, which is a struct ────────────────────────────────────
(defn- derive-object [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (defn- derive-object [c (Ptr Cursor) name string at-pos i32 src [const u8]] Derived
(when (at-byte? c \}) (when (at-byte? c \})
(return (derived-bad (return (derived-bad
(joined3 "the empty object at " (where src at-pos) (joined3 "the empty object at " (where src at-pos)
@ -349,7 +349,7 @@
(defn key=? [t Token s string] bool (defn key=? [t Token s string] bool
(bytes=? (.text t) (bytes-view s))) (bytes=? (.text t) (bytes-view s)))
(defn- has-escape? [s [u8]] bool (defn- has-escape? [s [const u8]] bool
(dotimes [i (length s)] (dotimes [i (length s)]
(when (= (at s i) \\) (when (= (at s i) \\)
(return true))) (return true)))
@ -358,7 +358,7 @@
;; What a field name may be made of. Deliberately narrower than what the reader ;; What a field name may be made of. Deliberately narrower than what the reader
;; would accept: this is the set a *person* would recognise as a name, and a ;; would accept: this is the set a *person* would recognise as a name, and a
;; member called "a b" or "x.y" has no field it could become. ;; member called "a b" or "x.y" has no field it could become.
(defn- name-like? [s [u8]] bool (defn- name-like? [s [const u8]] bool
(when (= (length s) 0) (when (= (length s) 0)
(return false)) (return false))
(dotimes [i (length s)] (dotimes [i (length s)]
@ -372,7 +372,7 @@
;; A copy of a token's raw text as a string. The Vec header is dropped here on ;; A copy of a token's raw text as a string. The Vec header is dropped here on
;; purpose: this runs inside the compiler, where an expansion is bounded by the ;; purpose: this runs inside the compiler, where an expansion is bounded by the
;; size of the program being compiled. ;; size of the program being compiled.
(defn- copy-of [s [u8]] string (defn- copy-of [s [const u8]] string
(let [b (vec-new u8)] (let [b (vec-new u8)]
(append (addr b) s) (append (addr b) s)
(string (slice b)))) (string (slice b))))
@ -424,7 +424,7 @@
_ (refuse "defjson's first argument is the name of the struct to declare, written as a name")) _ (refuse "defjson's first argument is the name of the struct to declare, written as a name"))
_ (refuse "defjson's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) _ (refuse "defjson's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from"))))
(defn- provide [name string path string src [u8]] Form (defn- provide [name string path string src [const u8]] Form
(let [cur (cursor src) (let [cur (cursor src)
d (derive (addr cur) name src)] d (derive (addr cur) name src)]
(if (bad? d) (if (bad? d)
@ -441,7 +441,7 @@
;; built in: a string field is a copy and a Vec field is an ;; built in: a string field is a copy and a Vec field is an
;; allocation, and spec-memory's rule is that the destination is ;; allocation, and spec-memory's rule is that the destination is
;; never implicit. ;; never implicit.
(defn ~bname [b [u8] a Allocator] ~sname (defn ~bname [b [const u8] a Allocator] ~sname
(let [c (cursor b) (let [c (cursor b)
out (~rname (addr c) a)] out (~rname (addr c) a)]
;; The cursor is made and dropped here, so this is the only ;; The cursor is made and dropped here, so this is the only

View File

@ -811,7 +811,7 @@
;; false for it either way, so a caller that checks sees the same thing. ;; false for it either way, so a caller that checks sees the same thing.
(defonce no-image Image) (defonce no-image Image)
(defn load-image-from-memory [file-type string data [u8]] Image (defn load-image-from-memory [file-type string data [const u8]] Image
(if (= (length data) 0) (if (= (length data) 0)
no-image no-image
(load-image-from-memory-raw file-type (addr (at data 0)) (length data)))) (load-image-from-memory-raw file-type (addr (at data 0)) (length data))))
@ -1606,7 +1606,7 @@
;; before it and `codepoint-size` is that one's length in bytes, so the ;; before it and `codepoint-size` is that one's length in bytes, so the
;; previous offset is `offset` minus what comes back through the pointer. At ;; previous offset is `offset` minus what comes back through the pointer. At
;; offset 0 there is nothing behind it and raylib is not asked. ;; offset 0 there is nothing behind it and raylib is not asked.
(defn get-codepoint-previous [text [u8] offset i32 codepoint-size (Ptr i32)] i32 (defn get-codepoint-previous [text [const u8] offset i32 codepoint-size (Ptr i32)] i32
(if (<= offset 0) (if (<= offset 0)
(do (set (deref codepoint-size) 0) 0) (do (set (deref codepoint-size) 0) 0)
(get-codepoint-previous-raw (addr (at text offset)) codepoint-size))) (get-codepoint-previous-raw (addr (at text offset)) codepoint-size)))

View File

@ -1,5 +1,5 @@
(defstruct Cursor (defstruct Cursor
[src [u8] ; a non-owning slice [src [const u8] ; a read-only, non-owning slice
pos i32]) ; no initialiser means zeroed pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8 (defn peek [c (Ptr Cursor)] u8

View File

@ -384,7 +384,8 @@ described.</p>
<li><strong>Fixed arrays are values.</strong> <code>[n T]</code> is inline storage <li><strong>Fixed arrays are values.</strong> <code>[n T]</code> is inline storage
and copies on assignment and on pass-by-value.</li> and copies on assignment and on pass-by-value.</li>
<li><strong>Slices are views.</strong> <code>[T]</code> is ptr+len and owns nothing. <li><strong>Slices are views.</strong> <code>[T]</code> is ptr+len and owns nothing.
Copying a slice copies the view, never the elements.</li> Copying a slice copies the view, never the elements. <code>[const T]</code> is the
same view with no stores through it.</li>
<li><strong>Pointers are visible.</strong> <code>(addr x)</code> takes the address of <li><strong>Pointers are visible.</strong> <code>(addr x)</code> takes the address of
any assignable place and gives <code>(Ptr T)</code>. It does not extend anything's any assignable place and gives <code>(Ptr T)</code>. It does not extend anything's
lifetime, and keeping one past its frame is your contract to honour — there is no 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.</p>
<tr><td><code>bool</code></td><td></td><td><code>i1</code></td></tr> <tr><td><code>bool</code></td><td></td><td><code>i1</code></td></tr>
<tr><td><code>string</code></td><td>a byte slice with no NUL</td><td>ptr + len</td></tr> <tr><td><code>string</code></td><td>a byte slice with no NUL</td><td>ptr + len</td></tr>
<tr><td><code>[T]</code></td><td>slice, non-owning</td><td>ptr + len</td></tr> <tr><td><code>[T]</code></td><td>slice, non-owning</td><td>ptr + len</td></tr>
<tr><td><code>[const T]</code></td><td>read-only slice: a <code>[T]</code> converts to one, never the reverse</td><td>ptr + len</td></tr>
<tr><td><code>[n T]</code></td><td>fixed array, a value</td><td>n inline items</td></tr> <tr><td><code>[n T]</code></td><td>fixed array, a value</td><td>n inline items</td></tr>
<tr><td><code>(Vec T)</code></td><td>growable, owning — copies as its header, so the copies alias one buffer</td><td>ptr + len + cap + its allocator</td></tr> <tr><td><code>(Vec T)</code></td><td>growable, owning — copies as its header, so the copies alias one buffer</td><td>ptr + len + cap + its allocator</td></tr>
<tr><td><code>(Map K V)</code></td><td>open addressing, owning, copies the same way. The only map spelling: braces in type position are not a type</td><td>data + len + log2cap + its allocator</td></tr> <tr><td><code>(Map K V)</code></td><td>open addressing, owning, copies the same way. The only map spelling: braces in type position are not a type</td><td>data + len + log2cap + its allocator</td></tr>
@ -800,7 +802,7 @@ code on every target, which is what makes the collector work there.</p>
its fields, and omitted fields are zeroed.</p> its fields, and omitted fields are zeroed.</p>
<pre><code>(defstruct Cursor <pre><code>(defstruct Cursor
[src [u8] ; a non-owning slice [src [const u8] ; a read-only, non-owning slice
pos i32]) ; no initialiser means zeroed pos i32]) ; no initialiser means zeroed
(defn peek [c (Ptr Cursor)] u8 (defn peek [c (Ptr Cursor)] u8
@ -829,8 +831,8 @@ its bytes.</p>
<p>There are two ways to see a string's bytes and the difference is whether anything <p>There are two ways to see a string's bytes and the difference is whether anything
is allocated. <code>(bytes-view s)</code> is the string's own storage seen as a is allocated. <code>(bytes-view s)</code> is the string's own storage seen as a
<code>[u8]</code> and costs nothing; it aliases the string, so a literal's view points <code>[const u8]</code> and costs nothing; it aliases the string, and a store through
into <code>.rodata</code> and writing through it traps. <code>(bytes s)</code> and it is a compile error. <code>(bytes s)</code> and
<code>(bytes s allocator)</code> make a writable copy through the allocator — never a <code>(bytes s allocator)</code> make a writable copy through the allocator — never a
hidden <code>malloc</code>, which is the rule every allocating operation follows. The hidden <code>malloc</code>, which is the rule every allocating operation follows. The
example above wants a view and takes one.</p> example above wants a view and takes one.</p>