type-of answers a value's kind as a keyword or an instance's class name, and a class named like a kind is refused.

This commit is contained in:
Joseph Ferano 2026-09-25 22:18:36 +07:00
parent 8aaf33589e
commit e80411d283
12 changed files with 134 additions and 8 deletions

View File

@ -20,11 +20,6 @@ Dyn text stays immutable, with chars and text converting to and from a dyn
vector of characters; length and indexing count characters on dyn text and bytes on vector of characters; length and indexing count characters on dyn text and bytes on
str. Waits on the dyn-unless-annotated design. str. Waits on the dyn-unless-annotated design.
** NEXT type-of
Decided 2026-09-25: (type-of x) answers a keyword — :nil :bool :int :float :text :vec
:map :keyword, :char once dyn has one, and a class instance's class name as class-of
does; a class named like a built-in kind is refused.
** NEXT Any typed container crosses into dyn as a view ** NEXT Any typed container crosses into dyn as a view
Decided 2026-09-25: every element type (all numbers, chars, structs, nested arrays) Decided 2026-09-25: every element type (all numbers, chars, structs, nested arrays)
and any storage; a dev build checks a view against its frame or allocation and traps and any storage; a dev build checks a view against its frame or allocation and traps

View File

@ -167,7 +167,7 @@ face says.")
;; Map ;; Map
"map-new" "put" "get" "map-remove" "map-next" "has-key?" "map-new" "put" "get" "map-remove" "map-next" "has-key?"
;; dyn ;; dyn
"class-of" "keyword" "class-of" "type-of" "keyword"
;; compile time ;; compile time
"embed" "embed-dir" "compile-error" "embed" "embed-dir" "compile-error"
;; files ;; files

View File

@ -11266,6 +11266,19 @@ and named_call ?(qualified = false) ctx ~want loc name args =
(rt loc Types.Dyn "flan_dyn_class_of" [ check ctx ~want:Types.Dyn v ]) (rt loc Types.Dyn "flan_dyn_class_of" [ check ctx ~want:Types.Dyn v ])
| _ -> assert false) | _ -> assert false)
(* (type-of v) -> the value's kind as a keyword, or a class instance's
class name. A typed argument crosses into dyn first, as it does for
class-of and every other dyn builtin, so it answers the kind the crossing
makes of it: a typed i32 is :int and an f32 is :float. That keeps one
answer per value whichever side of the program holds it. *)
| "type-of" ->
arity ctx loc name 1 args;
(match args with
| [ v ] ->
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_type_of" [ check ctx ~want:Types.Dyn v ])
| _ -> assert false)
(* (map-remove m k) -> (Option V): the value that was there, or None when (* (map-remove m k) -> (Option V): the value that was there, or None when
the key was not. The same answer [get] gives, for the same reason — a key the key was not. The same answer [get] gives, for the same reason — a key
that is not in the map is an answer and not a failure — and the value that is not in the map is an answer and not a failure — and the value
@ -13757,6 +13770,11 @@ let builtins : (string * string * string) list =
It is what a defgeneric dispatches on, so a class dispatcher is this \ 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) \ call over the first argument and a defmulti whose body is (class-of x) \
is the same generic function written the other way."); is the same generic function written the other way.");
("type-of", "type-of [dyn] dyn",
"The value's kind as a keyword — :nil :bool :int :float :text :vec \
:keyword :map — or, for a value built by a defclass constructor, the \
class's name as class-of answers it. A typed value answers the kind it \
has as a dyn value: an i32 is :int.");
("keyword", "keyword [string|[const u8]] dyn", ("keyword", "keyword [string|[const u8]] dyn",
"The interned dyn keyword named by the bytes, for a name that only \ "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 \ exists at run time — a reader building :texture-path out of a token's \

View File

@ -4993,6 +4993,7 @@ declare void @flan_dyn_slot_init(i64, i64, i64, ptr, i64)
declare void @flan_dyn_ctor_site(ptr, i64) declare void @flan_dyn_ctor_site(ptr, i64)
declare void @flan_dyn_map_put(i64, i64, i64, ptr, i64) declare void @flan_dyn_map_put(i64, i64, i64, ptr, i64)
declare i64 @flan_dyn_class_of(i64) declare i64 @flan_dyn_class_of(i64)
declare i64 @flan_dyn_type_of(i64)
declare void @flan_dyn_class_def(i64, ptr, i64) declare void @flan_dyn_class_def(i64, ptr, i64)
declare void @flan_dyn_class_hook(ptr) declare void @flan_dyn_class_hook(ptr)
declare i64 @flan_dyn_kw(ptr, i64) declare i64 @flan_dyn_kw(ptr, i64)

View File

@ -1682,6 +1682,22 @@ let rec decl (f : Form.t) : Ast.decl =
[[x y]] two dyn slots, [[pause bool step bool]] two typed ones — and [[x y]] two dyn slots, [[pause bool step bool]] two typed ones — and
is carried undecided for the same reason. *) is carried undecided for the same reason. *)
| [ n; { v = Vec slots; _ } ] -> | [ n; { v = Vec slots; _ } ] ->
(* type-of answers a class instance's class name and any other value's
kind, so a class named like a kind would make :map (say) mean two
things. Checked before [tname] so bool, int and float get this
sentence too. char is reserved for dyn's char kind. *)
(match n.v with
| Sym s
when List.mem s
[ "nil"; "bool"; "int"; "float"; "text"; "vec"; "map";
"keyword"; "char" ] ->
Loc.failk "parse/class-named-kind" n.loc
"%s is the name of a kind of dyn value, so (type-of x) would \
answer :%s both for an instance of this class and for a plain \
%s. Name the class %s-value, or any name that is not one of \
nil bool int float text vec map keyword char"
s s s s
| _ -> ());
List.iter List.iter
(fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ()) (fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ())
slots; slots;

View File

@ -2295,6 +2295,27 @@ flan_dyn flan_dyn_class_of(flan_dyn v) {
return dyn_make(BOX_KW, (uint64_t)(uintptr_t)o->u.v.klass); return dyn_make(BOX_KW, (uint64_t)(uintptr_t)o->u.v.klass);
} }
/* The kind of a value as a keyword named by [tag_words], or a class instance's
* class name as [flan_dyn_class_of] answers it. A class may not be named like
* a kind (the parser refuses it), so :map always means a plain map. Keywords
* are immortal, so each kind's keyword is interned once and kept. When dyn
* gains a char, its tag gets a word in [tag_words] and :char falls out here
* with no change to this function. */
flan_dyn flan_dyn_type_of(flan_dyn v) {
static flan_dyn kinds[FLAN_DYN_TAG_MAP + 1];
static int interned;
int32_t t = flan_dyn_tag(v);
if (t == FLAN_DYN_TAG_MAP && dyn_obj(v)->u.v.klass != NULL)
return flan_dyn_class_of(v);
if (!interned) {
for (int i = 0; i <= FLAN_DYN_TAG_MAP; i++)
kinds[i] = flan_dyn_kw((const uint8_t *)tag_words[i],
(int64_t)strlen(tag_words[i]));
interned = 1;
}
return kinds[t];
}
/* ── Keywords ────────────────────────────────────────────────────────── /* ── Keywords ──────────────────────────────────────────────────────────
* *
* One global table, append-only, never freed: a keyword is a *name*, the set * One global table, append-only, never freed: a keyword is a *name*, the set

View File

@ -133,6 +133,11 @@ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
* — an ordinary map included. Never traps. */ * — an ordinary map included. Never traps. */
flan_dyn flan_dyn_class_of(flan_dyn v); flan_dyn flan_dyn_class_of(flan_dyn v);
/* The value's kind as a keyword — :nil :bool :int :float :text :vec :keyword
* :map — or, for a class instance, its class name as [flan_dyn_class_of]
* answers it. Never traps. */
flan_dyn flan_dyn_type_of(flan_dyn v);
/* A class definition, registered or re-registered: [name] is the class's name /* A class definition, registered or re-registered: [name] is the class's name
* as a keyword and [slots]/[n] is its slots packed into one string, a line * as a keyword and [slots]/[n] is its slots packed into one string, a line
* each, the slot's name and then — after a space, for a typed slot — its * each, the slot's name and then — after a space, for a typed slot — its

View File

@ -0,0 +1,36 @@
;;;; type-of: every value's kind as a keyword, and a class instance's class
;;;; name, which is what class-of answers for it.
(defclass point [x y])
;; A defmulti over type-of is dispatch on the kind of the first argument.
(defmulti describe [v] dyn (type-of v))
(defmethod describe :int [v] "a whole number")
(defmethod describe :text [v] "some text")
(defmethod describe :point [v] "a point")
(defmethod describe :else [v] "something else")
(defn main [] i32
(let [n (the i32 7)
f (the f32 1.5)
b (the bool true)
s (the string "typed")]
(println (type-of nil))
(println (type-of true))
(println (type-of 42))
(println (type-of 2.5))
(println (type-of "hello"))
(println (type-of [1 2 3]))
(println (type-of {:a 1}))
(println (type-of :k))
(println (type-of (point 1 2)))
;; Typed values cross into dyn first and answer the kind they become.
(println (type-of n))
(println (type-of f))
(println (type-of b))
(println (type-of s))
(println (describe 3))
(println (describe "x"))
(println (describe (point 3 4)))
(println (describe [1]))
(if (= (type-of (point 0 0)) (class-of (point 0 0))) 0 1)))

View File

@ -5298,6 +5298,18 @@ level "1"
"programs/dyn-class.flan" dyn_class_out; "programs/dyn-class.flan" dyn_class_out;
outputs ~x86:true "dyn: classes and dispatch, --x86" outputs ~x86:true "dyn: classes and dispatch, --x86"
"programs/dyn-class.flan" dyn_class_out; "programs/dyn-class.flan" dyn_class_out;
(* type-of over each kind, a class instance, four typed values crossing
into dyn, and a defmulti dispatching on it. *)
let dyn_type_of_out =
":nil\n:bool\n:int\n:float\n:text\n:vec\n:map\n:keyword\n:point\n\
:int\n:float\n:bool\n:text\n\
a whole number\nsome text\na point\nsomething else\n"
in
outputs "dyn: type-of" "programs/dyn-type-of.flan" dyn_type_of_out;
outputs ~opt:"-O0" "dyn: type-of, -O0"
"programs/dyn-type-of.flan" dyn_type_of_out;
outputs ~x86:true "dyn: type-of, --x86"
"programs/dyn-type-of.flan" dyn_type_of_out;
(* (watch "name" v) with nothing arming the table: a struct, an array, a (* (watch "name" v) with nothing arming the table: a struct, an array, a
slice, a dyn map and a string all compile against flan_dev.c's watch slice, a dyn map and a string all compile against flan_dev.c's watch
entry points on both backends, write nothing, and evaluate the value entry points on both backends, write nothing, and evaluate the value

View File

@ -9215,7 +9215,11 @@ let () =
answers what it always did, which is what keeps every method answers what it always did, which is what keeps every method
ever written for this class reachable. *) ever written for this class reachable. *)
holds "the instance is still an instance of its class" holds "the instance is still an instance of its class"
"(if (= (class-of (at instances 1)) :point) 1 0)" "(if (= (class-of (at instances 1)) :point) 1 0)";
holds "type-of answers the class name in a session"
"(if (= (type-of (at instances 1)) :point) 1 0)";
holds "and a kind for anything that is not an instance"
"(if (= (type-of (get (at instances 1) :w)) :nil) 1 0)"
end; end;
(* ── A definition that did not change ── (* ── A definition that did not change ──

View File

@ -2974,6 +2974,20 @@ let () =
"(defclass marker [])\n(defn main [] i32 (let [m (marker)] 0))"; "(defclass marker [])\n(defn main [] i32 (let [m (marker)] 0))";
accepts "class-of answers nil for anything that is not an instance" accepts "class-of answers nil for anything that is not an instance"
"(defn main [] i32 (if (= (class-of 1) nil) 0 1))"; "(defn main [] i32 (if (= (class-of 1) nil) 0 1))";
(* type-of answers a class instance's name, so a class may not take the
name of a kind: :map would mean a plain map and an instance at once.
bool, int and float are also built-in types; they get this sentence. *)
List.iter
(fun k ->
rejects_check ("a class may not be named " ^ k)
(Printf.sprintf "(defclass %s [a])\n(defn main [] i32 0)" k)
~needle:(Printf.sprintf "Name the class %s-value" k))
[ "nil"; "bool"; "int"; "float"; "text"; "vec"; "map"; "keyword"; "char" ];
accepts "the fix a class-named-kind refusal offers compiles"
"(defclass map-value [a])\n\
(defn main [] i32 (if (= (type-of (map-value 1)) :map-value) 0 1))";
rejects_check "type-of takes one argument"
"(defn main [] i32 (let [k (type-of 1 2)] 0))" ~needle:"type-of";
(* The constructor is an ordinary function, so its arity is the ordinary (* The constructor is an ordinary function, so its arity is the ordinary
arity check and a wrong one names the class. *) arity check and a wrong one names the class. *)
rejects_check "a constructor takes one argument per slot" rejects_check "a constructor takes one argument per slot"

View File

@ -720,7 +720,11 @@ and a slot may be <code>bool</code>, an integer type, <code>f32</code>,
<code>f64</code>, <code>string</code>, a class, or <code>(Option T)</code> of one of <code>f64</code>, <code>string</code>, a class, or <code>(Option T)</code> of one of
those, which also admits <code>nil</code>. The constructor is the class's own name those, which also admits <code>nil</code>. The constructor is the class's own name
and is positional, and <code>class-of</code> answers the tag, or <code>nil</code> and is positional, and <code>class-of</code> answers the tag, or <code>nil</code>
for anything that is not an instance. The slots are map keys: <code>get</code> for anything that is not an instance. <code>type-of</code> answers any value's
kind as a keyword — <code>:nil</code>, <code>:bool</code>, <code>:int</code>,
<code>:float</code>, <code>:text</code>, <code>:vec</code>, <code>:map</code> or
<code>:keyword</code> — and an instance's class name, so a class cannot be named
after one of those kinds. The slots are map keys: <code>get</code>
reads one, and <code>set</code> writes one, as in reads one, and <code>set</code> writes one, as in
<code>(set (get s :pause) true)</code>. <code>put</code> writes one too, and is <code>(set (get s :pause) true)</code>. <code>put</code> writes one too, and is
also how a key the class does not declare is added.</p> also how a key the class does not declare is added.</p>