The dot advice says assign where the form is an assignment

(set p.x 1) was being told that a field is read with an accessor, which is a
sentence that does not apply to the form it is printed under. The place
spelling is (set (.x p) 1), checked to be a real form.
This commit is contained in:
Joseph Ferano 2026-09-20 18:14:58 +07:00
parent 859e3aa7f4
commit 16f90a7ef5
2 changed files with 24 additions and 12 deletions

View File

@ -4413,8 +4413,8 @@ and value_candidates ctx =
dot-infix habit.
The second is the near miss, over values only see [value_candidates]. *)
and unknown_name : 'a. ctx -> Loc.t -> string -> 'a =
fun ctx loc name ->
and unknown_name : 'a. ?setting:bool -> ctx -> Loc.t -> string -> 'a =
fun ?(setting = false) ctx loc name ->
let dot = String.index_opt name '.' in
let head, field =
match dot with
@ -4440,25 +4440,32 @@ and unknown_name : 'a. ctx -> Loc.t -> string -> 'a =
| Some sn, _ ->
let s = Option.get (fields_named ctx.env sn) in
let notes = declared_note ctx.env sn in
(* In a [set] the accessor is the *place*, so the spelling to give is
[(set (.x p) 1)] and not [(.x p)] on its own. Saying "read" at an
assignment would be a sentence that does not apply to the form it is
printed under. *)
let how =
if setting then Printf.sprintf "a field is assigned through an \
accessor, so write (set (.%s %s) ...)"
field head
else Printf.sprintf "a field is read with an accessor, so write (.%s %s)"
field head
in
if Tast.field_index s field <> None then
Loc.failk "check/dot-access" loc ~notes
"unknown name %s — a field is read with an accessor, so write (.%s %s)"
name field head
Loc.failk "check/dot-access" loc ~notes "unknown name %s — %s" name how
else
Loc.failk "check/dot-access" loc ~notes
"unknown name %s — a field is read with an accessor, (.%s %s), and \
%s has no field %s"
name field head sn field
"unknown name %s — %s, and %s has no field %s" name how sn field
| None, Some t ->
Loc.failk "check/dot-access" loc
"unknown name %s — a dot is part of the name here, not field access. \
Fields are read with an accessor, (.%s %s), and %s is %s, which has \
no fields"
A field is reached through an accessor, (.%s %s), and %s is %s, \
which has no fields"
name field head head (Types.to_string t)
| None, None ->
Loc.failk "check/unknown-name" loc
"unknown name %s — nothing named %s is in scope either. A field is \
read with an accessor, (.%s %s), not with a dot"
reached through an accessor, (.%s %s), not with a dot"
name head field head
end
else
@ -4578,7 +4585,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
into the image and there is nothing to assign to. Declare it with \
defvar if it has to change" name
| Some (ty, false) -> Tast.Pglobal name, ty
| None -> captured ctx loc name; unknown_name ctx loc name)
| None -> captured ctx loc name; unknown_name ~setting:true ctx loc name)
| Ast.Pfield (target, name) ->
let target, sname = struct_target ctx target in
let s = Option.get (fields_named ctx.env sname) in

View File

@ -1739,6 +1739,11 @@ let () =
rejects_check "and says so when the field is not there either"
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] p.z))"
~needle:"(.z p), and P has no field z";
rejects_check "and in a set it is the place that is spelled"
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] (set p.x 2) 0))"
~needle:"a field is assigned through an accessor, so write (set (.x p) ...)";
accepts "which is a real form"
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] (set (.x p) 2) (.x p)))";
rejects_check "a dotted head that is not a struct says what it is"
"(defn f [] i32 (let [n 1] n.x))" ~needle:"n is i32, which has no fields";
(* A capitalised head keeps the case spelling it always had: [Shape.Circle]