A package struct can be a return type

The parser decides "return type or first body form?" from the set of type
names the file declares, and an import is resolved after parsing - so a
package's structs cannot be in that set by construction. (defn mk [] rl/Vector2
...) therefore read the return type as the body and failed with "unknown name
rl/Vector2", which names the symptom and not the cause.

The signal is the alias plus the capital, and both halves are needed. An alias
is syntactically obvious and the same pre-pass collects it. A bare capitalised
symbol is never a value in this language - a struct or union constructor is
(Name {...}), a List, and an enum member is a keyword - so the hazard the
surrounding comment warns about, a body form eaten as a return type, has no
form of this shape to eat. A lowercase qualified name stays an expression,
which is what rl/get-color has to be.

Found by the raylib lane, which hit it on rl/Vector2 and reported it rather
than reaching into a file it did not own.
This commit is contained in:
Joseph Ferano 2026-09-12 03:59:57 +07:00
parent 71877a5baa
commit 90d3d6694e
3 changed files with 67 additions and 2 deletions

View File

@ -772,12 +772,35 @@ let rec decl types (f : Form.t) : Ast.decl =
pre-pass in [program] collects from the file's own declarations. That makes pre-pass in [program] collects from the file's own declarations. That makes
it exact rather than heuristic, because types are only ever introduced by it exact rather than heuristic, because types are only ever introduced by
defstruct, defunion and defalias all syntactically obvious. *) defstruct, defunion and defalias all syntactically obvious. *)
(* A name a package brought in — [rl/Vector2]. It cannot be in [types]: the set
comes from this file's own declarations, and an import is not resolved until
after parsing, so a package's structs are unknown here by construction.
The signal is the alias plus the capital. An alias is syntactically obvious,
collected by the same pre-pass; and a bare capitalised symbol is never a
*value* in this language a struct or union constructor is [(Name {...})],
a List, and an enum member is a keyword. So [alias/Name] in a type position
is a type, and the case the comment above warns about a body form eaten as
a return type cannot arise, because there is no body form of that shape.
A lowercase qualified name stays an expression, which is what [rl/get-color]
in [(defn f [] rl/get-color)] has to be. *)
and qualified_type types s =
match String.index_opt s '/' with
| None -> false
| Some i ->
let alias = String.sub s 0 i and name = String.sub s (i + 1) (String.length s - i - 1) in
Names.mem ("import " ^ alias) types
&& name <> ""
&& name.[0] >= 'A' && name.[0] <= 'Z'
and is_type_form types (f : Form.t) = and is_type_form types (f : Form.t) =
match f.v with match f.v with
| Sym s -> Names.mem s types | Sym s -> Names.mem s types || qualified_type types s
| Vec _ -> true (* [T] and [n T] are only types *) | Vec _ -> true (* [T] and [n T] are only types *)
| Map _ -> true (* {K V} in this position *) | Map _ -> true (* {K V} in this position *)
| List ({ v = Sym n; _ } :: _) -> Names.mem n types | List ({ v = Sym n; _ } :: _) ->
Names.mem n types || qualified_type types n
| _ -> false | _ -> false
and variant (f : Form.t) : Ast.variant = and variant (f : Form.t) : Ast.variant =
@ -797,6 +820,11 @@ let declared_types (forms : Form.t list) : Names.t =
match f.v with match f.v with
| List [ { v = Sym ("defstruct" | "defunion" | "defalias"); _ }; | List [ { v = Sym ("defstruct" | "defunion" | "defalias"); _ };
{ v = Sym n; _ }; _ ] -> Names.add n acc { v = Sym n; _ }; _ ] -> Names.add n acc
(* The aliases too, under a key no symbol can collide with, so that
[qualified_type] can tell [rl/Vector2] from a name with a slash in
it that nothing imported. *)
| List [ { v = Sym "import"; _ }; { v = Sym a; _ }; { v = Str _; _ } ] ->
Names.add ("import " ^ a) acc
| _ -> acc) | _ -> acc)
builtin_types forms builtin_types forms

View File

@ -0,0 +1,27 @@
;;;; A package-qualified struct in return position.
;;;;
;;;; The parser decides "is this form a return type or the first body form?"
;;;; from the set of type names the file declares — and an import is resolved
;;;; after parsing, so a package's structs cannot be in that set. The result
;;;; was that (defn mk [] rl/Vector2 ...) read the return type as the body and
;;;; failed with "unknown name rl/Vector2", which named the symptom and not the
;;;; cause. The alias plus the capital is the signal: a bare capitalised symbol
;;;; is never a value here, since a constructor is (Name {...}) and an enum
;;;; member is a keyword.
(import edn "vendor:edn")
(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))
;;; And the one that must keep working: a lowercase qualified name in the same
;;; position is an expression, not a type.
(defn local [] Local (Local {:n 5}))
(defn main [] i32
(let [c (fresh (bytes "[1 2]"))
t (edn/next (addr c))]
(print-i64 (i64 (.kind t))) (newline))
(print-i64 (i64 (.n (local)))) (newline)
0)

View File

@ -1033,6 +1033,16 @@ ERR@7 unexpected token: not the kind the caller was reading
outputs ~opt:"-O0" "destructuring, -O0" "programs/destructure.flan" outputs ~opt:"-O0" "destructuring, -O0" "programs/destructure.flan"
destructure_out; destructure_out;
(* A package-qualified struct as a declared return type, which the parser
used to read as the first body form. Reported by the raylib lane, which
hit it on rl/Vector2 and worked around it rather than reaching into a
file it did not own. *)
let pkgret_out = "9\n5\n" in
outputs "a package struct in return position" "programs/pkg-return.flan"
pkgret_out;
outputs ~opt:"-O0" "a package struct in return position, -O0"
"programs/pkg-return.flan" pkgret_out;
if !failures = 0 then print_endline "acceptance: all tests passed" if !failures = 0 then print_endline "acceptance: all tests passed"
else begin else begin
Printf.printf "\n%d failure(s)\n" !failures; Printf.printf "\n%d failure(s)\n" !failures;