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

\[mav_1 (x) = \mathtt{ShiftMap}(\mathtt{MapReduce}(\mathtt{mulacc}))(x)\]

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.

main.yaml
 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).

genspec_leafs.yaml
 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.

digraph G {
	graph [bb="0,0,966.29,1040",
		fontname=Verdana,
		rankdir=LR
	];
	node [label="\N"];
	subgraph "cluster_cluster_main.main" {
		graph [bb="0.15274,8,958.29,1032",
			color=black,
			fillcolor=white,
			label="main : Generic.Generic.Infinite",
			lheight=0.21,
			lp="479.22,1020.5",
			lwidth=2.96,
			shape=record,
			style=filled
		];
		subgraph "cluster_cluster_main.main.ReadArray" {
			graph [bb="586.2,16,837.08,145",
				fillcolor=lightgrey,
				label="ReadArray : Generic.IO.ReadArray",
				lheight=0.21,
				lp="711.64,133.5",
				lwidth=3.26,
				shape=record,
				style=filled
			];
			"main.main.ReadArray-arg"	[height=0.5,
				label=input,
				pos="711.14,96",
				shape=oval,
				width=0.97491];
			"main.main.ReadArray-format"	[height=0.5,
				label=format,
				pos="711.14,42",
				shape=parallelogram,
				width=1.7206];
		}
		subgraph "cluster_cluster_main.main.PrintArray" {
			graph [bb="296.31,872,543.19,1001",
				fillcolor=lightgrey,
				label="PrintArray : Generic.IO.PrintArray",
				lheight=0.21,
				lp="419.75,989.5",
				lwidth=3.21,
				shape=record,
				style=filled
			];
			"main.main.PrintArray-arg"	[height=0.5,
				label=output,
				pos="419.25,898",
				shape=oval,
				width=1.1555];
			"main.main.PrintArray-format"	[height=0.5,
				label=format,
				pos="419.25,952",
				shape=parallelogram,
				width=1.7206];
		}
		subgraph "cluster_cluster_main.main.mav_1" {
			graph [bb="8.1527,153,757.49,864",
				fillcolor=lightgrey,
				label="mav_1 : Skeleton.Compute.ShiftFarm",
				lheight=0.21,
				lp="382.82,852.5",
				lwidth=3.56,
				shape=record,
				style=filled
			];
			subgraph "cluster_cluster_main.main.mav_1.fred_1" {
				graph [bb="16.153,261,481.19,735",
					color=black,
					fillcolor=white,
					label="fred_1 : Skeleton.Compute.FarmRed_Acc",
					lheight=0.21,
					lp="248.67,723.5",
					lwidth=3.89,
					shape=record,
					style=filled
				];
				subgraph "cluster_cluster_main.main.mav_1.fred_1.mulacc" {
					graph [bb="24.153,369,237.15,606",
						fillcolor=lightgrey,
						label="mulacc : zoti_gen.core.Block",
						lheight=0.21,
						lp="130.65,594.5",
						lwidth=2.74,
						shape=record,
						style=filled
					];
					"main.main.mav_1.fred_1.mulacc-in1"	[height=0.5,
						label=in1,
						pos="130.15,395",
						shape=oval,
						width=0.75];
					"main.main.mav_1.fred_1.mulacc-in2"	[height=0.5,
						label=in2,
						pos="130.15,449",
						shape=oval,
						width=0.75];
					"main.main.mav_1.fred_1.mulacc-acc"	[height=0.5,
						label=acc,
						pos="130.15,557",
						shape=oval,
						width=0.75];
					"main.main.mav_1.fred_1.mulacc-out"	[height=0.5,
						label=out,
						pos="130.15,503",
						shape=oval,
						width=0.75];
				}
				"main.main.mav_1.fred_1-in1"	[height=0.5,
					label="input+_it",
					pos="419.25,341",
					shape=oval,
					width=1.4985];
				"main.main.mav_1.fred_1-in2"	[height=0.5,
					label=COEF,
					pos="419.25,449",
					shape=oval,
					width=1.0652];
				"main.main.mav_1.fred_1-size1"	[height=0.5,
					label=_range,
					pos="130.15,632",
					shape=oval,
					width=1.1735];
				"main.main.mav_1.fred_1-out1"	[height=0.5,
					label="output[_it]",
					pos="130.15,686",
					shape=oval,
					width=1.661];
				"main.main.mav_1.fred_1-_acc"	[height=0.5,
					label=_acc,
					pos="419.25,530",
					shape=oval,
					width=0.84854];
				"main.main.mav_1.fred_1-_it"	[height=0.5,
					label=_it0,
					pos="130.15,341",
					shape=oval,
					width=0.77632];
				"main.main.mav_1.fred_1-iterate_over"	[height=0.5,
					label=iterate_over,
					pos="130.15,287",
					shape=parallelogram,
					width=2.7265];
				"main.main.mav_1.fred_1.mulacc-in1" -> "main.main.mav_1.fred_1-in1"	[pos="e,371.88,349.72 156.38,390.26 202.07,381.66 299.54,363.33 361.82,351.61"];
				"main.main.mav_1.fred_1.mulacc-in2" -> "main.main.mav_1.fred_1-in2"	[pos="e,380.52,449 157.39,449 205.68,449 309.25,449 370.36,449"];
				"main.main.mav_1.fred_1.mulacc-acc" -> "main.main.mav_1.fred_1-_acc"	[pos="e,388.71,532.78 157.14,554.56 207.53,549.82 318.81,539.35 378.53,533.73"];
				"main.main.mav_1.fred_1.mulacc-out" -> "main.main.mav_1.fred_1-_acc"	[pos="e,388.71,527.22 157.14,505.44 207.53,510.18 318.81,520.65 378.53,526.27"];
			}
			"main.main.mav_1-in1"	[height=0.5,
				label=input,
				pos="711.14,287",
				shape=oval,
				width=0.97491];
			"main.main.mav_1-out1"	[height=0.5,
				label=output,
				pos="419.25,815",
				shape=oval,
				width=1.1555];
			"main.main.mav_1-COEF"	[height=0.5,
				label=COEF,
				pos="711.14,449",
				shape=oval,
				width=1.0652];
			"main.main.mav_1-_it"	[height=0.5,
				label=_it,
				pos="130.15,233",
				shape=oval,
				width=0.75];
			"main.main.mav_1-_range"	[height=0.5,
				label=_range,
				pos="419.25,761",
				shape=oval,
				width=1.1735];
			"main.main.mav_1-iterate_over"	[height=0.5,
				label=iterate_over,
				pos="130.15,179",
				shape=parallelogram,
				width=2.7265];
			"main.main.mav_1.fred_1-in1" -> "main.main.mav_1-in1"	[pos="e,678.16,293.25 467.27,332.73 490.18,328.66 518.14,323.64 543.19,319 585.79,311.11 634.44,301.74 668.18,295.19"];
			"main.main.mav_1.fred_1-in2" -> "main.main.mav_1-COEF"	[pos="e,672.61,449 457.85,449 510.2,449 605.06,449 662.36,449"];
			"main.main.mav_1.fred_1-size1" -> "main.main.mav_1-_range"	[pos="e,376.82,761.05 171.56,635.74 192.54,639.23 217.77,645.97 237.15,659 273.85,683.67 258.75,715.65 296.31,739 317.14,751.95 343.61,\
757.73 366.56,760.16"];
			"main.main.mav_1.fred_1-out1" -> "main.main.mav_1-out1"	[pos="e,379.35,809.8 152.84,702.74 183.24,725.39 241.04,765.57 296.31,788 319.48,797.41 346.52,803.82 369.25,808.02"];
		}
		"main.main-input"	[height=0.5,
			label=input,
			pos="915.19,191",
			shape=oval,
			width=0.97491];
		"main.main-output"	[height=0.5,
			label=output,
			pos="711.14,894",
			shape=oval,
			width=1.1555];
		"main.main-schedule"	[height=0.5,
			label=schedule,
			pos="130.15,79",
			shape=parallelogram,
			width=2.1176];
		"main.main-myformat"	[height=0.5,
			label=myformat,
			pos="711.14,952",
			shape=parallelogram,
			width=2.2765];
		"main.main.ReadArray-arg" -> "main.main-input"	[pos="e,891.21,177.72 739.78,106.82 765.33,117.04 804.2,133.12 837.08,149 852.14,156.27 868.45,165.02 882.17,172.65"];
		"%3"	[height=0.20833,
			pos="419.25,42",
			shape=plain,
			width=0.31944];
		"%3" -> "main.main.ReadArray-format"	[pos="e,661.8,42 430.91,42 466.15,42 580.7,42 651.67,42"];
		"main.main.PrintArray-arg" -> "main.main-output"	[pos="e,669.33,894.56 460.86,897.44 512.79,896.72 602.73,895.48 659.2,894.7"];
		"main.main.PrintArray-format" -> "main.main-myformat"	[pos="e,645.93,952 468.77,952 514.33,952 583.15,952 635.66,952"];
		"main.main.mav_1-in1" -> "main.main-input"	[pos="e,888.81,203.06 737.73,274.84 773.71,257.75 839.35,226.56 879.64,207.42"];
		"main.main.mav_1-out1" -> "main.main-output"	[pos="e,672.48,887.14 452.08,826.24 485.28,837.71 538.97,855.54 586.2,868 611.2,874.6 639.38,880.62 662.55,885.2"];
	}
}

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.

Generic/__init__.py
 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)
Generic/IO.py
 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
Generic/templates.c
 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.

Skeleton/Compute.py
 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.

Skeleton/templates.c
 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).

Dependencies
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.

Output C code
 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.

zoticonf.json
[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}"