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