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
it exact rather than heuristic, because types are only ever introduced by
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) =
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 *)
| 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
and variant (f : Form.t) : Ast.variant =
@ -797,6 +820,11 @@ let declared_types (forms : Form.t list) : Names.t =
match f.v with
| List [ { v = Sym ("defstruct" | "defunion" | "defalias"); _ };
{ 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)
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"
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"
else begin
Printf.printf "\n%d failure(s)\n" !failures;