The three ways an embed is written wrong, each said at the right moment

A computed path, a file that is not there, and a second argument that is
not `string`. The type argument is now settled before the file is
opened: a program asking for a type embed cannot read a file as was
otherwise told the file was missing, and got the real complaint only
after fixing the wrong thing.

A missing asset is a compile error naming it rather than an empty embed,
because an asset silently absent is the class of quiet wrongness the
whole feature exists to remove. An empty *directory* is not that: it
embeds cleanly as [0 EmbedFile] and len answers 0.
This commit is contained in:
Joseph Ferano 2026-09-12 11:43:34 +07:00
parent fff4f5d985
commit fe85ecd246
2 changed files with 30 additions and 4 deletions

View File

@ -2038,6 +2038,17 @@ and named_call ctx ~want loc name args =
| "embed" ->
(match args with
| [ p ] | [ p; _ ] ->
(* The spelling is settled before the file is opened, so a program that
asks for a type embed cannot read a file as is told that, rather than
being told the file is missing and left to discover the other half
after fixing it. *)
(match args with
| [ _; { Ast.e = Ast.Var "string"; _ } ] | [ _ ] -> ()
| [ _; t ] ->
fail t.Ast.loc
"embed's second argument is the type to read the file as, and \
`string` is the only one (embed \"p\") is the [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]
@ -2055,10 +2066,6 @@ and named_call ctx ~want loc name args =
(match args with
| [ _; { Ast.e = Ast.Var "string"; _ } ] ->
expect loc ~want (as_string ())
| [ _; t ] ->
fail t.Ast.loc
"embed's second argument is the type to read the file as, and \
`string` is the only one (embed \"p\") is the [u8]"
| _ ->
(match want with
| Some Types.String -> as_string ()

View File

@ -360,6 +360,25 @@ let () =
"(defn main [] i32 (restart-case 0 (use-value [v i32] v))\n\
\ (invoke-restart 'use-value (println \"\")) 0)"
"a restart argument must be a value";
(* An embed reads the bytes before any value exists, so the path has to be
a literal - Odin's rule and for Odin's reason (check_load_directive
refuses anything that is not Addressing_Constant). This is the refusal
that keeps the result genuinely free at run time. *)
refuses_src "an embedded path that is computed"
"(defn main [] i32 (let [p \"x\"] (len (embed p))))"
"must be a literal string";
(* A file that is not there is a compile error naming it, not an empty
embed: an asset silently missing is the class of quiet wrongness this
whole feature exists to remove. *)
refuses_src "an embedded file that does not exist"
"(defn main [] i32 (len (embed \"no-such-asset.bin\")))"
"cannot embed";
(* One extra argument, and `string` is the only thing it can be. Two
spellings, not one form that changes type with its context. *)
refuses_src "embed asked for a type it cannot read a file as"
"(defn main [] i32 (len (embed \"no-such-asset.bin\" i32)))"
"`string` is the only one";
(* Allocators, spec-memory.md. The tier on its own, with no container
above it, so that a failure here is not read as a Vec bug. What is
asserted is the capability set differing per allocator, the context