logo

drewdevault.com

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

Hare-codegen-v2.md (8930B)


  1. ---
  2. title: Codegen in Hare v2
  3. date: 2022-11-26
  4. ---
  5. I spoke about code generation in Hare [back in May][0] when I wrote a tool for
  6. generating ioctl numbers. I wrote another code generator over the past few
  7. weeks, and it seems like a good time to revisit the topic on my blog to showcase
  8. another approach, and the improvements we've made for this use-case.
  9. [0]: https://drewdevault.com/2022/05/14/generating-ioctls.html
  10. In this case, I wanted to generate code to implement IPC (inter-process
  11. communication) interfaces for my operating system. I have designed a <abbr
  12. title="domain-specific language">DSL</abbr> for describing these interfaces
  13. &mdash; you can [read the grammar here][1]. This calls for a parser, which is
  14. another interesting topic for Hare, but I'll set that aside for now and focus on
  15. the code gen. Assume that, given a file like the following, we can parse it and
  16. produce an AST:
  17. [1]: https://git.sr.ht/~sircmpwn/ipcgen/tree/master/item/doc/grammar.txt
  18. ```
  19. namespace hello;
  20. interface hello {
  21. call say_hello() void;
  22. call add(a: uint, b: uint) uint;
  23. };
  24. ```
  25. The key that makes the code gen approach we're looking at today is the
  26. introduction of [strings::template][2] to the Hare standard library. This module
  27. is inspired by a similar feature from Python, [string.Template][3]. An example
  28. of its usage is provided in Hare's standard library documentation:
  29. [2]: https://docs.harelang.org/strings/template
  30. [3]: https://docs.python.org/3/library/string.html#template-strings
  31. ```hare
  32. const src = "Hello, $user! Your balance is $$$balance.\n";
  33. const template = template::compile(src)!;
  34. defer template::finish(&template);
  35. template::execute(&template, os::stdout,
  36. ("user", "ddevault"),
  37. ("balance", 1000),
  38. )!; // "Hello, ddevault! Your balance is $1000.
  39. ```
  40. Makes sense? Cool. Let's see how this can be applied to code generation. The
  41. interface shown above compiles to the following generated code:
  42. ```hare
  43. // This file was generated by ipcgen; do not modify by hand
  44. use errors;
  45. use helios;
  46. use rt;
  47. def HELLO_ID: u32 = 0xC01CAAC5;
  48. export type fn_hello_say_hello = fn(object: *hello) void;
  49. export type fn_hello_add = fn(object: *hello, a: uint, b: uint) uint;
  50. export type hello_iface = struct {
  51. say_hello: *fn_hello_say_hello,
  52. add: *fn_hello_add,
  53. };
  54. export type hello_label = enum u64 {
  55. SAY_HELLO = HELLO_ID << 16u64 | 1,
  56. ADD = HELLO_ID << 16u64 | 2,
  57. };
  58. export type hello = struct {
  59. _iface: *hello_iface,
  60. _endpoint: helios::cap,
  61. };
  62. export fn hello_dispatch(
  63. object: *hello,
  64. ) void = {
  65. const (tag, a1) = helios::recvraw(object._endpoint);
  66. switch (rt::label(tag): hello_label) {
  67. case hello_label::SAY_HELLO =>
  68. object._iface.say_hello(
  69. object,
  70. );
  71. match (helios::reply(0)) {
  72. case void =>
  73. yield;
  74. case errors::invalid_cslot =>
  75. yield; // callee stored the reply
  76. case errors::error =>
  77. abort(); // TODO
  78. };
  79. case hello_label::ADD =>
  80. const rval = object._iface.add(
  81. object,
  82. a1: uint,
  83. rt::ipcbuf.params[1]: uint,
  84. );
  85. match (helios::reply(0, rval)) {
  86. case void =>
  87. yield;
  88. case errors::invalid_cslot =>
  89. yield; // callee stored the reply
  90. case errors::error =>
  91. abort(); // TODO
  92. };
  93. case =>
  94. abort(); // TODO
  95. };
  96. };
  97. ```
  98. Generating this code starts with the following entry-point:
  99. ```hare
  100. // Generates code for a server to implement the given interface.
  101. export fn server(out: io::handle, doc: *ast::document) (void | io::error) = {
  102. fmt::fprintln(out, "// This file was generated by ipcgen; do not modify by hand")!;
  103. fmt::fprintln(out, "use errors;")!;
  104. fmt::fprintln(out, "use helios;")!;
  105. fmt::fprintln(out, "use rt;")!;
  106. fmt::fprintln(out)!;
  107. for (let i = 0z; i < len(doc.interfaces); i += 1) {
  108. const iface = &doc.interfaces[i];
  109. s_iface(out, doc, iface)?;
  110. };
  111. };
  112. ```
  113. Here we start with some simple use of basic string formatting via
  114. [fmt::fprintln][fmt::fprintln]. We see some of the same approach repeated in the
  115. meatier functions like s\_iface:
  116. [fmt::fprintln]: https://docs.harelang.org/fmt#fprintln
  117. ```hare
  118. fn s_iface(
  119. out: io::handle,
  120. doc: *ast::document,
  121. iface: *ast::interface,
  122. ) (void | io::error) = {
  123. const id: ast::ident = [iface.name];
  124. const name = gen_name_upper(&id);
  125. defer free(name);
  126. let id: ast::ident = alloc(doc.namespace...);
  127. append(id, iface.name);
  128. defer free(id);
  129. const hash = genhash(&id);
  130. fmt::fprintfln(out, "def {}_ID: u32 = 0x{:X};\n", name, hash)!;
  131. ```
  132. Our first use of strings::template appears when we want to generate type aliases
  133. for interface functions, via s\_method\_fntype. This is where some of the
  134. trade-offs of this approach begin to present themselves.
  135. ```hare
  136. const s_method_fntype_src: str =
  137. `export type fn_$iface_$method = fn(object: *$object$params) $result;`;
  138. let st_method_fntype: tmpl::template = [];
  139. @init fn s_method_fntype() void = {
  140. st_method_fntype= tmpl::compile(s_method_fntype_src)!;
  141. };
  142. fn s_method_fntype(
  143. out: io::handle,
  144. iface: *ast::interface,
  145. meth: *ast::method,
  146. ) (void | io::error) = {
  147. assert(len(meth.caps_in) == 0); // TODO
  148. assert(len(meth.caps_out) == 0); // TODO
  149. let params = strio::dynamic();
  150. defer io::close(&params)!;
  151. if (len(meth.params) != 0) {
  152. fmt::fprint(&params, ", ")?;
  153. };
  154. for (let i = 0z; i < len(meth.params); i += 1) {
  155. const param = &meth.params[i];
  156. fmt::fprintf(&params, "{}: ", param.name)!;
  157. ipc_type(&params, &param.param_type)!;
  158. if (i + 1 < len(meth.params)) {
  159. fmt::fprint(&params, ", ")!;
  160. };
  161. };
  162. let result = strio::dynamic();
  163. defer io::close(&result)!;
  164. ipc_type(&result, &meth.result)!;
  165. tmpl::execute(&st_method_fntype, out,
  166. ("method", meth.name),
  167. ("iface", iface.name),
  168. ("object", iface.name),
  169. ("params", strio::string(&params)),
  170. ("result", strio::string(&result)),
  171. )?;
  172. fmt::fprintln(out)?;
  173. };
  174. ```
  175. The simple string substitution approach of strings::template prevents it from
  176. being as generally useful as a full-blown templating engine ala jinja2. To work
  177. around this, we have to write Hare code which does things like slurping up the
  178. method parameters into a [strio::dynamic] buffer where we might instead reach
  179. for something like
  180. <code>{%&nbsp;for&nbsp;param&nbsp;in&nbsp;method.params&nbsp;%}</code> in
  181. jinja2. Once we have prepared all of our data in a format suitable for a linear
  182. string substitution, we can pass it to
  183. <abbr title="This file aliases strings::template as tmpl to simplify things a bit.">tmpl</abbr>::execute.
  184. The actual template is stored in a global which is compiled during @init, which
  185. runs at program startup. Anything which requires a loop to compile, such as the
  186. parameter list, is fetched out of the strio buffer and passed to the template.
  187. [strio::dynamic]: https://docs.harelang.org/strio#dynamic
  188. We can explore a slightly different approach when we generate this part of the
  189. code, back up in the s\_iface function:
  190. ```hare
  191. export type hello_iface = struct {
  192. say_hello: *fn_hello_say_hello,
  193. add: *fn_hello_add,
  194. };
  195. ```
  196. To output this code, we render several templates one after another, rather than
  197. slurping up the generated code into heap-allocated string buffers to be passed
  198. into a single template.
  199. ```hare
  200. const s_iface_header_src: str =
  201. `export type $iface_iface = struct {`;
  202. let st_iface_header: tmpl::template = [];
  203. const s_iface_method_src: str =
  204. ` $method: *fn_$iface_$method,`;
  205. let st_iface_method: tmpl::template = [];
  206. @init fn s_iface() void = {
  207. st_iface_header = tmpl::compile(s_iface_header_src)!;
  208. st_iface_method = tmpl::compile(s_iface_method_src)!;
  209. };
  210. // ...
  211. tmpl::execute(&st_iface_header, out,
  212. ("iface", iface.name),
  213. )?;
  214. fmt::fprintln(out)?;
  215. for (let i = 0z; i < len(iface.methods); i += 1) {
  216. const meth = &iface.methods[i];
  217. tmpl::execute(&st_iface_method, out,
  218. ("iface", iface.name),
  219. ("method", meth.name),
  220. )?;
  221. fmt::fprintln(out)?;
  222. };
  223. fmt::fprintln(out, "};\n")?;
  224. ```
  225. The [remainder of the code][4] is fairly similar.
  226. [4]: https://git.sr.ht/~sircmpwn/ipcgen/tree/2cdc53095a052b4f5ce3fdc6e410f2dd17eea54d/item/gen/server.ha
  227. strings::template is less powerful than a more sophisticated templating system
  228. might be, such as Golang's text/template. A more sophisticated templating engine
  229. could be implemented for Hare, but it would be more challenging &mdash; no
  230. reflection or generics in Hare &mdash; and would not be a great candidate for
  231. the standard library. This approach hits the sweet spot of simplicity and
  232. utility that we're aiming for in the Hare stdlib. strings::template is
  233. implemented in [a single ~180 line file][5].
  234. [5]: https://git.sr.ht/~sircmpwn/hare/tree/da003e45ced4991b1bae282169dcf942e1e4b235/item/strings/template/template.ha
  235. I plan to continue polishing this tool so I can use it to describe interfaces
  236. for communications between userspace drivers and other low-level userspace
  237. services in my operating system. If you have any questions, feel free to post
  238. them on my public inbox, or shoot them over to my new [fediverse account][6].
  239. Until next time!
  240. [6]: https://fosstodon.org/@drewdevault