logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

Reflection.gmi (10550B)


  1. Note: this is a redacted copy of a blog post published on the internal development blog of a new systems programming language. The name of the project and further details are deliberately being kept in confidence until the initial release. You may be able to find it if you look hard enough — you have my thanks in advance for keeping it to yourself. For more information, see “We are building a new systems programming language”.
  2. I’ve just merged support for reflection in ****. Here’s how it works!
  3. ## Background
  4. “Reflection” refers to the ability for a program to examine the type system of its programming language, and to dynamically manipulate types and their values at runtime. You can learn more at Wikipedia:
  5. => https://en.wikipedia.org/wiki/Reflective_programming Reflective programming
  6. ## Reflection from a user perspective
  7. Let’s start with a small sample program:
  8. ```
  9. use fmt;
  10. use types;
  11. export fn main() void = {
  12. const my_type: type = type(int);
  13. const typeinfo: *types::typeinfo = types::reflect(my_type);
  14. fmt::printfln("int\nid: {}\nsize: {}\nalignment: {}",
  15. typeinfo.id, typeinfo.sz, typeinfo.al)!;
  16. };
  17. ```
  18. Running this program produces the following output:
  19. ```
  20. int
  21. id: 1099590421
  22. size: 4
  23. alignment: 4
  24. ```
  25. This gives us a simple starting point to look at. We can see that “type” is used as the type of the “my_type” variable, and initialized with a “type(int)” expression. This expression returns a type value for the type given in the parenthesis — in this case, for the “int” type.
  26. To learn anything useful, we have to convert this to a “types::typeinfo” pointer, which we do via types::reflect. The typeinfo structure looks like this:
  27. ```
  28. type typeinfo = struct {
  29. id: uint,
  30. sz: size,
  31. al: size,
  32. flags: flags,
  33. repr: repr,
  34. };
  35. ```
  36. The ID field is the type’s unique identifier, which is universally unique and deterministic, and forms part of ****’s ABI. This is derived from an FNV-32 hash of the type information. You can find the ID for any type by modifying our little example program, or you can use the helper program in the cmd/****type directory of the **** source tree.
  37. Another important field is the “repr” field, which is short for “representation”, and it gives details about the inner structure of the type. The repr type is defined as a tagged union of all possible type representations in the **** type system:
  38. ```
  39. type repr = (alias | array | builtin | enumerated | func | pointer | slice | struct_union | tagged | tuple);
  40. ```
  41. In the case of the “int” type, the representation is “builtin”:
  42. ```
  43. type builtin = enum uint {
  44. BOOL, CHAR, F32, F64, I16, I32, I64, I8, INT, NULL, RUNE, SIZE, STR, U16, U32,
  45. U64, U8, UINT, UINTPTR, VOID, TYPE,
  46. };
  47. ```
  48. builtin::INT, in this case. The structure and representation of the “int” type is defined by the **** specification and cannot be overridden by the program, so no further information is necessary.
  49. More information is provided for more complex types, such as structs.
  50. ```
  51. use fmt;
  52. use types;
  53. export fn main() void = {
  54. const my_type: type = type(struct {
  55. x: int,
  56. y: int,
  57. });
  58. const typeinfo: *types::typeinfo = types::reflect(my_type);
  59. fmt::printfln("id: {}\nsize: {}\nalignment: {}",
  60. typeinfo.id, typeinfo.sz, typeinfo.al)!;
  61. const st = typeinfo.repr as types::struct_union;
  62. assert(st.kind == types::struct_kind::STRUCT);
  63. for (let i = 0z; i < len(st.fields); i += 1) {
  64. const field = st.fields[i];
  65. assert(field.type_ == type(int));
  66. fmt::printfln("\t{}: offset {}", field.name, field.offs)!;
  67. };
  68. };
  69. ```
  70. The output of this program is:
  71. ```
  72. id: 2617358403
  73. size: 8
  74. alignment: 4
  75. x: offset 0
  76. y: offset 4
  77. ```
  78. Here the “repr” field provides the “types::struct_union” structure:
  79. ```
  80. type struct_union = struct {
  81. kind: struct_kind,
  82. fields: []struct_field,
  83. };
  84. type struct_kind = enum {
  85. STRUCT,
  86. UNION,
  87. };
  88. type struct_field = struct {
  89. name: str,
  90. offs: size,
  91. type_: type,
  92. };
  93. ```
  94. Makes sense? Excellent. So how does it all work?
  95. ## Reflection internals
  96. Let me first draw the curtain back from the magic “types::reflect” function:
  97. ```
  98. // Returns [[typeinfo]] for the provided type.
  99. export fn reflect(in: type) const *typeinfo = in: *typeinfo;
  100. ```
  101. It simply casts the “type” value to a pointer, which is what it is. When the compiler sees an expression like let x = type(int), it statically allocates the typeinfo data structure into the program and returns a pointer to it, which is then wrapped up in the opaque “type” meta-type. The “reflect” function simply converts it to a useful pointer. Here’s the generated IR for this:
  102. ```
  103. %binding.4 =l alloc8 8
  104. storel $rt.builtin_int, %binding.4
  105. ```
  106. A clever eye will note that we initialize the value to a pointer to “rt.builtin_int”, rather than allocating a typeinfo structure here and now. The runtime module provides static typeinfos for all built-in types, which look like this:
  107. ```
  108. export const @hidden builtin_int: types::typeinfo = types::typeinfo {
  109. id = 1099590421,
  110. sz = 4, al = 4, flags = 0,
  111. repr = types::builtin::INT,
  112. };
  113. ```
  114. These are an internal implementation detail, hence “@hidden”. But many types are not built-in, so the compiler is required to statically allocate a typeinfo structure:
  115. ```
  116. export fn main() void = {
  117. let x = type(struct { x: int, y: int });
  118. };
  119. ```
  120. ```
  121. data $strdata.7 = section ".data.strdata.7" { b "x" }
  122. data $strdata.8 = section ".data.strdata.8" { b "y" }
  123. data $sldata.6 = section ".data.sldata.6" {
  124. l $strdata.7, l 1, l 1, l 0, l $rt.builtin_int,
  125. l $strdata.8, l 1, l 1, l 4, l $rt.builtin_int,
  126. }
  127. data $typeinfo.5 = section ".data.typeinfo.5" {
  128. w 2617358403, z 4,
  129. l 8,
  130. l 4,
  131. w 0, z 4,
  132. w 5555256, z 4,
  133. w 0, z 4,
  134. l $sldata.6, l 2, l 2,
  135. }
  136. export function section ".text.main" "ax" $main() {
  137. @start.0
  138. %binding.4 =l alloc8 8
  139. @body.1
  140. storel $typeinfo.5, %binding.4
  141. @.2
  142. ret
  143. }
  144. ```
  145. This has the unfortunate effect of re-generating all of these typeinfo structures every time someone uses type(struct { x: int, y: int }). We still have one trick up our sleeve, though: type aliases! Most people don’t actually use anonymous structs like this often, preferring to use a type alias to give them a name like “coords”. When they do this, the situation improves:
  146. ```
  147. type coords = struct { x: int, y: int };
  148. export fn main() void = {
  149. let x = type(coords);
  150. };
  151. ```
  152. ```
  153. data $strdata.1 = section ".data.strdata.1" { b "coords" }
  154. data $sldata.0 = section ".data.sldata.0" { l $strdata.1, l 6, l 6 }
  155. data $strdata.4 = section ".data.strdata.4" { b "x" }
  156. data $strdata.5 = section ".data.strdata.5" { b "y" }
  157. data $sldata.3 = section ".data.sldata.3" {
  158. l $strdata.4, l 1, l 1, l 0, l $rt.builtin_int,
  159. l $strdata.5, l 1, l 1, l 4, l $rt.builtin_int,
  160. }
  161. data $typeinfo.2 = section ".data.typeinfo.2" {
  162. w 2617358403, z 4,
  163. l 8,
  164. l 4,
  165. w 0, z 4,
  166. w 5555256, z 4,
  167. w 0, z 4,
  168. l $sldata.3, l 2, l 2,
  169. }
  170. data $type.1491593906 = section ".data.type.1491593906" {
  171. w 1491593906, z 4,
  172. l 8,
  173. l 4,
  174. w 0, z 4,
  175. w 3241765159, z 4,
  176. l $sldata.0, l 1, l 1,
  177. l $typeinfo.2
  178. }
  179. export function section ".text.main" "ax" $main() {
  180. @start.6
  181. %binding.10 =l alloc8 8
  182. @body.7
  183. storel $type.1491593906, %binding.10
  184. @.8
  185. ret
  186. }
  187. ```
  188. The declaration of a type alias provides us with the perfect opportunity to statically allocate a typeinfo singleton for it. Any of these which go unused by the program are automatically stripped out by the linker thanks to the --gc-sections flag. Also note that a type alias is considered a distinct representation from the underlying struct type:
  189. ```
  190. type alias = struct {
  191. ident: []str,
  192. secondary: type,
  193. };
  194. ```
  195. This explains the differences in the structure of the “type.1491593906” global. The struct { x: int, y: int } type is the “secondary” field of this type.
  196. ## Future improvements
  197. This is just the first half of the equation. The next half is to provide useful functions to work with this data. One such example is “types::strenum”:
  198. ```
  199. // Returns the value of the enum at "val" as a string. Aborts if the value is
  200. // not present. Note that this does not work with enums being used as a flag
  201. // type, see [[strflag]] instead.
  202. export fn strenum(ty: type, val: *void) str = {
  203. const ty = unwrap(ty);
  204. const en = ty.repr as enumerated;
  205. const value: u64 = switch (en.storage) {
  206. case builtin::CHAR, builtin::I8, builtin::U8 =>
  207. yield *(val: *u8);
  208. case builtin::I16, builtin::U16 =>
  209. yield *(val: *u16);
  210. case builtin::I32, builtin::U32 =>
  211. yield *(val: *u32);
  212. case builtin::I64, builtin::U64 =>
  213. yield *(val: *u64);
  214. case builtin::INT, builtin::UINT =>
  215. yield switch (size(int)) {
  216. case 4 =>
  217. yield *(val: *u32);
  218. case 8 =>
  219. yield *(val: *u64);
  220. case => abort();
  221. };
  222. case builtin::SIZE =>
  223. yield switch (size(size)) {
  224. case 4 =>
  225. yield *(val: *u32);
  226. case 8 =>
  227. yield *(val: *u64);
  228. case => abort();
  229. };
  230. case => abort();
  231. };
  232. for (let i = 0z; i < len(en.values); i += 1) {
  233. if (en.values[i].1.u == value) {
  234. return en.values[i].0;
  235. };
  236. };
  237. abort("enum has invalid value");
  238. };
  239. ```
  240. This is used like so:
  241. ```
  242. use types;
  243. use fmt;
  244. type watchmen = enum {
  245. VIMES,
  246. CARROT,
  247. ANGUA,
  248. COLON,
  249. NOBBY = -1,
  250. };
  251. export fn main() void = {
  252. let officer = watchmen::ANGUA;
  253. fmt::println(types::strenum(type(watchmen), &officer))!; // Prints ANGUA
  254. };
  255. ```
  256. Additional work is required to make more useful tools like this. We will probably want to introduce a “value” abstraction which can store an arbitrary value for an arbitrary type, and helper functions to assign to or read from those values. A particularly complex case is likely to be some kind of helper for calling a function pointer via reflection, which we I may cover in a later article. There will also be some work to bring the “types” (reflection) module closer to the ****::* namespace, which already features ****::ast, ****::parse, and ****::types, so that the parser, type checker, and reflection systems are interopable and work together to implement the **** type system.
  257. Want to help us build this language? We are primarily looking for help in the following domains:
  258. * Architectures or operating systems, to help with ports
  259. * Compilers & language design
  260. * Cryptography implementations
  261. * Date & time implementations
  262. * Unix
  263. If you’re an expert in a domain which is not listed, but that you think we should know about, then feel free to reach out. Experts are perferred, motivated enthusiasts are acceptable. Send me an email if you want to help!