Mercerenies / gdlisp

Lisp on the Godot platform
GNU General Public License v3.0
141 stars 1 forks source link

Type Checking #43

Closed Mercerenies closed 3 years ago

Mercerenies commented 3 years ago

We need to clean up type-checking. Godot's type system is, to put it mildly, a large uncontrollable mass of duct tape and glue.

Here's a summary of Godot's typing rules, to the best of my understanding.

All of this is overly complicated. Currently, GDLisp defines one primitive for type checking: instance?. The current behavior of this function is as follows:

This works fairly well for the cases above, but it's still quite ad-hoc. I want a consistent GDLisp-side type hierarchy.


Here's what this issue proposes. The GDLisp-side type hierarchy is as follows. There is a single root type Any. The root type has two direct children: AnyVal and AnyRef.

The children of AnyVal are all of the primitive types, such as String and Int. Most children of AnyVal have trivial subtyping relations with one another. We'll define a Number type which is a common supertype to Int and Float, and we'll define BaseArray, the common parent to the several array types defined in Godot.

The children of AnyRef are all of the other types, including nodes, objects, and references. For the most part, children of AnyRef mirror the usual Godot type hierarchy, which Object being the only direct child, and its children are Node and Reference, and so on.

We define two type-checking functions: instance? (which behaves similarly to how it does now, but under the new type system) and typeof (which returns the most specific type for an instance).

Semantically, that describes a type system. And users of GDLisp should only care about the above paragraphs. I intend to hide all of the messier details from the users as much as possible. Now let's talk about implementation details.

As far as implementation goes, there are four "kinds" of types we're dealing with.

  1. Primitive types, such as Int or Float, which correspond to Godot TYPE_* constants.
  2. Godot built-in types like Node and Reference. Godot's runtime reification of these type names is as GDScriptNativeClass instances.
  3. User-defined scripts, usually instances of GDScript.
  4. Synthetic types, such as BaseArray and AnyVal, which exist only in GDLisp and have no official Godot presence on the GDScript side.

(1) are always concrete, final types (i.e. no subtypes). (2) and (3) follow conventional Java-like single-inheritance rules. Generally, all types in (2) or (3) are constructible (I can't think of a way offhand to make an abstract class in GDScript). (4) are always abstract classes and exist only for subtyping relationships.

Now, as for how these will be represented.

  1. Currently, Int is a name in GDLisp whose value is an integer constant (TYPE_INT, to be precise). I'll change this, so that all primitives (such as Int) are instances of a wrapper class (PrimitiveType).
  2. Built-in types are GDScriptNativeClass. I aim to leave these alone for now and use them as-is.
  3. User-defined scripts are instances of GDScript. Again, I aim to leave these alone.
  4. Synthetic types will be children of a SyntheticType class which I'll design.

instance? will work roughly as it does now. If the type argument is a PrimitiveType, check the typeof ID. If it's GDScript or GDScriptNativeClass, use is. If it's a synthetic type, use whatever special behavior the synthetic type defines (probably as a method on its class).

typeof will first check the Godot typeof. If it returns something other than TYPE_OBJECT, then an appropriate PrimitiveType will be returned. If it returns TYPE_OBJECT, then we check get_script. If get_script returns non-null, then return that value. Otherwise, we check get_class and compare the name against known GDScript class names to get the native instance.

Mercerenies commented 3 years ago

Minor issue: GDScriptNativeClass is a class which doesn't have a name, as far as Godot is concerned; I can't directly reference it. It may be possible to bootstrap a reference to the class from some other instance, but we may or may not have access to it.

Mercerenies commented 3 years ago

Attached is a diagram of the current type hierarchy I am considering, with the four categories of types from the comment above color-coded.

GDLisp_Types.odg

Mercerenies commented 3 years ago

type-checking shall be the tracking branch for this feature.

Mercerenies commented 3 years ago

81bc850 adds primitive types. Primitive types are instance variables on the GDLisp singleton (accessible from static scope using sys/declare trickery). They're instances of the private class PrimitiveType, which defines the satisfies? method.

Here is a full list of the primitives we define.

[1] Note: In the ODG table above, this is a subtype of AnyRef, unlike the other primitives. This will be special-cased going forward. [2] Note: This only covers GDScript values of type Array, not the more specialized ones. A common supertype will be added soon.

Mercerenies commented 3 years ago

A note, since it came up as a possible point of confusion during development.

Object is, as of right now, the sole direct subtype of AnyRef. As such, every AnyRef is an Object and vice versa right now. However, strictly speaking, we are treating Object as a subtype of AnyRef and not the other way around. Every value in GDLisp is an instance of exactly one of AnyRef or AnyVal. It so happens that, right now, every AnyRef is an Object. In the future, Godot might add a new root to its object hierarchy, or a new type of value which isn't primitive. Or GDLisp might add a new type of object that doesn't fit into Godot's traditional hierarchy. Those types would be new direct subtypes of AnyRef.

The point I'm trying to make is that right now AnyRef and Object are synonymous, but I reserve the right to change that later and to add new subtypes to AnyRef in the future. I won't be considering such changes breaking, since I make no guarantee that Object is the only direct subtype of AnyRef.

Mercerenies commented 3 years ago

ded23e9e0fc7ed5a15f3fb3d4dc36c3b33a2ac54 adds synthetic types. The following synthetic types exist.

Note that, currently, AnyRef does not include arrays or dictionaries, which as far as Godot is concerned are primitives. This is an unfortunate difference in what Godot considers "primitive" (everything that's not an Object), as opposed to what we intend by AnyVal (namely, the Scala notion of AnyVal includes only value types, those that are not passed by sharing)

Mercerenies commented 3 years ago

Just a reminder for the moment. = is only guaranteed to work on strings and numbers right now in GDLisp, so types should not be compared with =. In particular, the integer object you get from (typeof 3) may not be = equal to the object Int, only isomorphic to it. We have no generalized equality operators (#61) yet.

Mercerenies commented 3 years ago

22ddfbc5821f8ac4155280cd6d02def86383aef0 adds typeof (and 91b10356fd5c6ad6053868d9506002da24c23d3e adds testing for it). This issue's original proposal involved the implementation of four "kinds" of types: primitive types, script types, native class types, and synthetic types. It also proposed two functions: instance? and typeof. All of these features have been implemented.