diff --git a/TODO.org b/TODO.org index 1fc05579..d8ca6c01 100644 --- a/TODO.org +++ b/TODO.org @@ -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 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 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 diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 1efff081..24878704 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -167,7 +167,7 @@ face says.") ;; Map "map-new" "put" "get" "map-remove" "map-next" "has-key?" ;; dyn - "class-of" "keyword" + "class-of" "type-of" "keyword" ;; compile time "embed" "embed-dir" "compile-error" ;; files diff --git a/lib/check.ml b/lib/check.ml index a3065a85..9d0f833c 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 ]) | _ -> 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 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 @@ -13757,6 +13770,11 @@ let builtins : (string * string * string) list = 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) \ 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", "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 \ diff --git a/lib/emit.ml b/lib/emit.ml index fd4f6423..c451a247 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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_map_put(i64, i64, i64, ptr, 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_hook(ptr) declare i64 @flan_dyn_kw(ptr, i64) diff --git a/lib/parse.ml b/lib/parse.ml index 5a76c9ab..4651dd4c 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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 is carried undecided for the same reason. *) | [ 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 (fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ()) slots; diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index ea488a33..2a5516d3 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -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); } +/* 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 ────────────────────────────────────────────────────────── * * One global table, append-only, never freed: a keyword is a *name*, the set diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 7667650b..a029206a 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -133,6 +133,11 @@ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v, * — an ordinary map included. Never traps. */ 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 * 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 diff --git a/test/programs/dyn-type-of.flan b/test/programs/dyn-type-of.flan new file mode 100644 index 00000000..f5676b99 --- /dev/null +++ b/test/programs/dyn-type-of.flan @@ -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))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index dd1821ca..53e2390f 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -5298,6 +5298,18 @@ level "1" "programs/dyn-class.flan" dyn_class_out; outputs ~x86:true "dyn: classes and dispatch, --x86" "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 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 diff --git a/test/test_dev.ml b/test/test_dev.ml index 72a203bb..e854c406 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -9215,7 +9215,11 @@ let () = answers what it always did, which is what keeps every method ever written for this class reachable. *) 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; (* ── A definition that did not change ── diff --git a/test/test_flan.ml b/test/test_flan.ml index 90ce6d94..c1b4cf37 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2974,6 +2974,20 @@ let () = "(defclass marker [])\n(defn main [] i32 (let [m (marker)] 0))"; accepts "class-of answers nil for anything that is not an instance" "(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 arity check and a wrong one names the class. *) rejects_check "a constructor takes one argument per slot" diff --git a/web/index.html b/web/index.html index cd8e96ef..a82b34ce 100644 --- a/web/index.html +++ b/web/index.html @@ -720,7 +720,11 @@ and a slot may be bool, an integer type, f32, f64, string, a class, or (Option T) of one of those, which also admits nil. The constructor is the class's own name and is positional, and class-of answers the tag, or nil -for anything that is not an instance. The slots are map keys: get +for anything that is not an instance. type-of answers any value's +kind as a keyword — :nil, :bool, :int, +:float, :text, :vec, :map or +:keyword — and an instance's class name, so a class cannot be named +after one of those kinds. The slots are map keys: get reads one, and set writes one, as in (set (get s :pause) true). put writes one too, and is also how a key the class does not declare is added.