Tutorial¶
This tutorial goes through a simple example generating a C file from an
input block specification. For this example the running sources are
found in the tests/inputs
folder of the ZOTI-Gen project root.
The target file which we are trying to obtain in this tutorial is a C file representing the sequential implementation of a moving-average algorithm applied on a sequence of numbers. It consists in a composite pattern described functionally as
Specification Files¶
We define the code blocks structure for the implemented system in the
files main.yaml
representing module main
and genspec_leafs.yaml
representing module genspec_leafs
.
1module: main
2version: '0.1.0'
3description: Source for tutorial
4top: main
5---
6block:
7- name: main
8 type: {module: Generic, name: Infinite}
9 prototype: |
10 int {{ name }}(void) {
11 int input[10];
12 int output[10];
13 int COEF[4] = {1, 2, 2, 1};
14 {{placeholder['code']}}
15 }
16 label:
17 - {name: input, usage: "{{ label[p].name }}"}
18 - {name: output, usage: "{{ label[p].name }}"}
19 param:
20 schedule: ["readio", "exec", "writeio"]
21 myformat: "\"%d\"" # TEST
22 instance:
23 - placeholder: readio
24 block: {module: Generic.IO, name: ReadArray}
25 directive: ["expand"]
26 bind:
27 - value_to_param: {value: "\"%d\"", child: format}
28 - value_to_param: {value: "10", child: size}
29 - label_to_label: {parent: input, child: arg, usage: "{{ label[p].name }}"}
30 - placeholder: writeio
31 block: {module: Generic.IO, name: PrintArray}
32 directive: ["expand"]
33 bind:
34 - value_to_param: {value: "10", child: size}
35 - param_to_param: {child: format, parent: myformat}
36 - label_to_label: {child: arg, parent: output, usage: "{{ label[p].name }}"}
37 - placeholder: exec
38 block: {module: main, name: mav_1}
39 directive: ["expand"]
40 bind:
41 - label_to_label: {child: in1, parent: input, usage: "{{ label[p].name }}"}
42 - label_to_label: {child: out1, parent: output, usage: "{{ label[p].name }}"}
43 usage: "int _it;\nint _range;\n{{placeholder['code']}}"
44
45- name: mav_1
46 type: {module: Skeleton.Compute, name: ShiftFarm}
47 label:
48 - {name: in1, usage: "{{ label[p].name }}"}
49 - {name: out1, usage: "{{ label[p].name }}"}
50 - {name: COEF, usage: "{{ label[p].name }}"}
51 - {name: _it, usage: "{{ label[p].name }}"}
52 - {name: _range, usage: "{{ label[p].name }}"}
53 param:
54 iterate_over: [in1]
55 instance:
56 - placeholder: f
57 block: {module: main, name: fred_1}
58 directive: ["expand"]
59 bind:
60 - label_to_label:
61 {parent: in1, child: in1, usage: "{{ label[p].name }}+{{label._it.name}}"}
62 - label_to_label: {child: in2, parent: COEF, usage: "{{ label[p].name }}"}
63 - label_to_label: {child: size1, parent: _range, usage: "{{ label[p].name }}"}
64 - label_to_label:
65 {parent: out1, child: out1, usage: "{{ label[p].name }}[{{label._it.name}}]"}
66 usage: "int _it0;\nint _acc;\n{{placeholder['code']}}"
67
68- name: fred_1
69 type: {module: Skeleton.Compute, name: FarmRed_Acc}
70 label:
71 - {name: in1, usage: "{{ label[p].name }}"}
72 - {name: in2, usage: "{{ label[p].name }}"}
73 - {name: size1, usage: "{{ label[p].name }}"}
74 - {name: out1, usage: "{{ label[p].name }}"}
75 - {name: _acc, usage: "{{ label[p].name }}"}
76 - {name: _it, usage: "{{ label[p].name }}"}
77 param:
78 iterate_over:
79 size1: { range: "name"}
80 in2:
81 instance:
82 - placeholder: f
83 block: {module: genspec_leafs, name: mulacc}
84 # directive: ["expand"]
85 usage: "{{name}}({{label.in1.name}}, {{label.in2.name}}, {{label.acc.name}}, {{label.out.name}})"
86 bind:
87 - label_to_label:
88 parent: in1
89 child: in1
90 usage: "({{ label[p].name }})[{{label._it.name}}]"
91 - label_to_label:
92 parent: in2
93 child: in2
94 usage: "{{ label[p].name }}[{{label._it.name}}]"
95 - label_to_label: {child: acc, parent: _acc, usage: "{{ label[p].name }}"}
96 - label_to_label: {child: out, parent: _acc, usage: "{{ label[p].name }}"}
Notice the definition of mav_1
which is an implementation of
component Skeleton.Compute.ShiftFarm
(i.e., the shift-map pattern)
and instantiates block fred_1
which is an implementation of
Skeleton.Compute.FarmRed_Acc
(i.e., the map-reduce pattern with an
accumulator). The latter instantiates mulacc
defined below as a
regular block, containing the “native” kernel code in its code
entry. Also note that the top block main
implements a
Generic.Infinite
template (i.e., an infinite loop) scheduling three
placeholders as of its param/schedule
: readio
(for reading a
stream of input numbers), exec
(expanding the mav_1
block) and
writeio
(for printing the results to the terminal).
1module: genspec_leafs
2top: mulacc
3---
4block:
5- name: mulacc
6 label:
7 - {name: in1, usage: "{{ label[p].name }}"}
8 - {name: in2, usage: "{{ label[p].name }}"}
9 - {name: acc, usage: "{{ label[p].name }}"}
10 - {name: out, usage: "{{ label[p].name }}"}
11 prototype: "void {{name}}(in1, in2, acc, &out) {\n {{ placeholder['code'] }} \n};"
12 code:
13 out = acc + in1 * in2;
Notice that the input specification is as verbose as it needs to be to
facilitate the parsing of data. This is because these files are not
meant to be written by humans, much rather generated by upstream
tools[1]. Also, the convenient information in the usage
and
prototype
entries is also meant to be provided by an upstream
pre-processor with the help of a type system, but this is outside the
scope of ZOTI-Gen.
Bindings and Names¶
After resolving the structure here is how the block components look like. Pay attention to the label bindings, as well as their resolved names, as compared to their initial description.
Library Components¶
The library components are grouped into two modules (or packages):
Generic
for harboring generic templates, and Skeleton
for defining
templates for specialized collective operations, all targeting a
sequential C implementation.
Generic¶
The generic components are spread throughout several files showcasing some of the template design tips described in Template Libraries.
1from dataclasses import dataclass, field
2from zoti_gen import with_schema, Block
3
4
5@with_schema(Block.Schema)
6@dataclass
7class Infinite(Block):
8 """ The base C implementation for an infinite loop. """
9
10 code: str = field(
11 default="\
12while (1) { \n\
13 {% for inst in param.schedule %} \n\
14 {{ placeholder[inst] }} \n\
15 {% endfor %} \n\
16}"
17 )
18
19 def check(self):
20 assert "schedule" in self.param
21 assert isinstance(self.param["schedule"], list)
1from dataclasses import dataclass, field
2from zoti_gen import read_at, with_schema, Block, Requirement, Template, C_DELIMITERS
3
4file_args = {"module": __name__,
5 "filename": "templates.c",
6 "delimiters": C_DELIMITERS}
7
8
9@with_schema(Block.Schema)
10@dataclass
11class ReadArray(Block):
12 """Generic implementation for monitoring a system. Gets outside
13 information from the console.
14 """
15
16 requirement: Requirement = field(
17 default=Requirement({"include": ["stdio.h"]}))
18
19 code: str = field(
20 default=Template(
21 read_at(**file_args, name="ReadArray.C"),
22 parent="code")
23 )
24
25 def check(self):
26 assert "format" in self.param
27 assert "size" in self.param
28 assert "arg" in self.label
29
30
31@with_schema(Block.Schema)
32@dataclass
33class PrintArray(Block):
34 """Generic implementation for monitoring a system. Prints an array to
35 the console.
36 """
37
38 requirement: Requirement = field(
39 default=Requirement({"include": ["stdio.h"]}))
40
41 code: str = field(
42 default=Template(
43 read_at(**file_args, name="PrintArray.C"),
44 parent="code")
45 )
46
47 def check(self):
48 assert "format" in self.param
49 assert "size" in self.param
50 assert "arg" in self.label
1// Template: ReadArray.C
2{int __io_it;
3 for (__io_it = 0; __io_it < {{ param.size }}; __io_it++){
4 scanf({{ param.format }}, &{{ label.arg.name }}[__io_it]);
5 }
6}
7// End: ReadArray.C
8
9
10// Template: PrintArray.C
11{int __io_it;
12 for (__io_it = 0; __io_it < {{ param.size }}; __io_it++){
13 printf({{ param.format }}, {{ label.arg.name }}[__io_it]);
14 }
15}
16// End: PrintArray.C
17
Specialized Collective Operations¶
The Skeleton
module showcases more advanced template design using
the ZOTI-Gen Jinja extensions.
1from dataclasses import dataclass, field
2from zoti_gen import read_at, with_schema, Block, Requirement, Template, C_DELIMITERS
3
4
5file_args = {"module": __name__,
6 "filename": "templates.c",
7 "delimiters": C_DELIMITERS}
8
9
10@with_schema(Block.Schema)
11@dataclass
12class ShiftFarm(Block):
13 """
14 Shift-farm specialized skeleton
15 """
16
17 requirement: Requirement = field(
18 default=Requirement({"include": ["cutils.h"]}))
19
20 code: str = field(
21 default=Template(
22 read_at(**file_args, name="ShiftFarm.C"),
23 parent="code")
24 )
25
26 def check(self):
27 # internal iterator (resolved name)
28 assert "_it" in self.label
29
30 # auto-bound to 'size1' from placeholder? TODO
31 assert "_range" in self.label
32
33 # called template
34 assert "f" in [i.placeholder for i in self.instance]
35
36
37@with_schema(Block.Schema)
38@dataclass
39class FarmRed_Acc(Block):
40 """
41 Farm-reduce skeleton with initial element, fused map-reduce kernel function
42 and programmable (exposed) size param.
43 """
44
45 requirement: Requirement = field(
46 default=Requirement({"include": ["cutils.h"]}))
47
48 code: str = field(
49 default=Template(
50 read_at(**file_args, name="FarmRed_Acc.C"),
51 parent="code")
52 )
53
54 def check(self):
55 # internal iterator (resolved name)
56 assert "_it" in self.label
57
58 # auto-bound to 'size1' from placeholder? TODO
59 assert "_acc" in self.label
60
61 # label IDs over which '_it' iterates
62 assert "iterate_over" in self.param
63
64 # called fused farm-reduce kernel
65 assert "f" in [i.placeholder for i in self.instance]
Notice that both ShiftFarm
and FarmRed_Acc
specify a requirement
upon a certain target library header cutils.h
. This is where
functions used in their template such as min
are supposed to be
defined.
1// Template: ShiftFarm.C
2{% set iterate_over = param.iterate_over|setdefaults({"offset": "0", "range": "name" }) %}
3
4{% macro itrange() -%}
5{% set ranges = [] %}
6{% for k,p in iterate_over.items() -%}
7{% do ranges.append(label[k]|find(p.range)) %}
8{%- endfor %}min({{ ranges|join(", ") }})
9{%- endmacro %}
10
11for ({{ label._it.name }} = 0; {{ label._it.name }} < {{ itrange() }} ; {{ label._it.name }}++){
12 {{ label._range.name }} = {{ itrange() }} - {{ label._it.name }};
13 {{ placeholder["f"] }}
14}
15// End: ShiftFarm.C
16
17
18// Template: FarmRed_Acc.C
19{% set iterate_over = param.iterate_over|setdefaults({"offset": "0", "range": "name" }) %}
20
21{% macro itrange() -%}
22{% set ranges = [] %}
23{% for k,p in iterate_over.items() -%}
24{% do ranges.append(label[k]|find(p.range)) %}
25{%- endfor %}min({{ ranges|join(", ") }})
26{%- endmacro %}
27
28for ({{ label._it.name }} = 0; {{ label._it.name }} < {{ itrange() }} ; {{ label._it.name }}++){
29 {{ placeholder["f"] }}
30}
31{{ label.out1.name }} = {{ label._acc.name }};
32// End: FarmRed_Acc.C
The template definition shows an example of using local Jinja
macro,
as well as the in-house
zoti_gen.render.JinjaExtensions.setdefaults()
filter.
Output Artifacts¶
Finally, the generated artifacts can be seen in this section. In this case the artifacts are presented as two separate files:
a JSON file representing the include dependencies, useful for a post-processor, e.g. to generate the preamble of the .c file.
a .c file containing the bulk of the code (without the preamble).
1{"include": ["stdio.h", "cutils.h"]}
Notice that the generated code reflects the directive flags passed
during block instantiation: all blocks are expanded as inline text
except mulacc
which is not using the "expand"
directive. This is
because it contains plain non-template native code which needs to be
isolated as a separate function.
1void mulacc(in1, in2, acc, &out) {
2 // vvv mulacc
3 out = acc + in1 * in2;
4 // ^^^ mulacc
5};
6
7int main(void) {
8 int input[10];
9 int output[10];
10 int COEF[4] = {1, 2, 2, 1};
11 // vvv main
12 while (1) {
13
14 // vvv ReadArray
15
16 {
17 int __io_it;
18 for (__io_it = 0; __io_it < 10; __io_it++) {
19 scanf("%d", &input[__io_it]);
20 }
21 }
22 //
23 // ^^^ ReadArray
24
25 int _it;
26 int _range;
27 // vvv mav_1
28
29 for (_it = 0; _it < min(input); _it++) {
30 _range = min(input) - _it;
31 int _it0;
32 int _acc;
33 // vvv fred_1
34
35 for (_it0 = 0; _it0 < min(_range, COEF); _it0++) {
36 mulacc((input + _it)[_it0], COEF[_it0], _acc, _acc)
37 }
38 output[_it] = _acc;
39 //
40 // ^^^ fred_1
41 }
42 //
43 // ^^^ mav_1
44
45 // vvv PrintArray
46
47 {
48 int __io_it;
49 for (__io_it = 0; __io_it < 10; __io_it++) {
50 printf("%d", output[__io_it]);
51 }
52 }
53 //
54 // ^^^ PrintArray
55 }
56 // ^^^ main
57}
To obtain these files we ran the ZOTI-Gen CLI tool using the following configuration, assuming that all source files, libraries as well as the CLI tool are accessible from the current folder.
[zoti.gen]
lib = ["."]
deps = "deps.json"
input = ["main.yaml", "genspec_leafs.yaml"]
output = "out.c"
begin_block = "// vvv {comp.name}\n"
end_block = "\n// ^^^ {comp.name}"