flummi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flummi/IR/AST.py ADDED
@@ -0,0 +1,111 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass
3
+
4
+ from .common import (
5
+ Expression,
6
+ Label,
7
+ Located,
8
+ Type,
9
+ Variable,
10
+ )
11
+ from .common import (
12
+ Program as BaseProgram,
13
+ )
14
+
15
+ __all__ = (
16
+ "Assignment",
17
+ "Block",
18
+ "Break",
19
+ "Conditional",
20
+ "Continue",
21
+ "Declaration",
22
+ "Emit",
23
+ "Expression",
24
+ "Fork",
25
+ "Gather",
26
+ "Label",
27
+ "Loop",
28
+ "NoOp",
29
+ "Program",
30
+ "Statement",
31
+ "Stop",
32
+ "Sync",
33
+ "Type",
34
+ "Variable",
35
+ )
36
+
37
+
38
+ class Statement(Located, ABC): ...
39
+
40
+
41
+ @dataclass
42
+ class Declaration(Statement):
43
+ variable: Variable
44
+ type: Type
45
+
46
+
47
+ @dataclass
48
+ class Block(Statement):
49
+ statements: list[Statement]
50
+
51
+
52
+ @dataclass
53
+ class Assignment(Statement):
54
+ variable: Variable
55
+ expression: Expression
56
+
57
+
58
+ @dataclass
59
+ class Emit(Statement):
60
+ variable: Variable
61
+
62
+
63
+ @dataclass
64
+ class Stop(Statement): ...
65
+
66
+
67
+ @dataclass
68
+ class NoOp(Statement): ...
69
+
70
+
71
+ @dataclass
72
+ class Conditional(Statement):
73
+ condition: Variable
74
+ true_branch: Statement
75
+ false_branch: Statement
76
+
77
+
78
+ @dataclass
79
+ class Loop(Statement):
80
+ label: Label
81
+ body: Statement
82
+
83
+
84
+ @dataclass
85
+ class Continue(Statement):
86
+ label: Label
87
+
88
+
89
+ @dataclass
90
+ class Break(Statement):
91
+ label: Label
92
+
93
+
94
+ @dataclass
95
+ class Fork(Statement):
96
+ variables: list[Variable]
97
+ expression: Expression
98
+
99
+
100
+ @dataclass
101
+ class Gather(Statement):
102
+ aggregates: dict[Variable, Expression]
103
+ keys: list[Variable]
104
+
105
+
106
+ @dataclass
107
+ class Sync(Statement):
108
+ keys: list[Variable]
109
+
110
+
111
+ Program = BaseProgram[Statement]
flummi/IR/CFP.py ADDED
@@ -0,0 +1,94 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass
3
+
4
+ from ..library import graph
5
+ from .common import Expression, Label, Located, Variable
6
+ from .common import Program as BaseProgram
7
+
8
+ __all__ = (
9
+ "Assignment",
10
+ "Emit",
11
+ "Expression",
12
+ "Fork",
13
+ "Gather",
14
+ "Jump",
15
+ "IsSynced",
16
+ "Label",
17
+ "Label",
18
+ "Merge",
19
+ "Plan",
20
+ "Primitive",
21
+ "Program",
22
+ "Start",
23
+ "Stop",
24
+ "Variable",
25
+ "Where",
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class Plan(Located):
31
+ entry_label: Label
32
+ primitives: dict[Label, Primitive]
33
+ successors_of: graph.Graph[Label]
34
+ virtual_successors_of: graph.Graph[Label]
35
+
36
+
37
+ @dataclass
38
+ class Primitive(Located, ABC): ...
39
+
40
+
41
+ @dataclass
42
+ class Start(Primitive): ...
43
+
44
+
45
+ @dataclass
46
+ class Stop(Primitive): ...
47
+
48
+
49
+ @dataclass
50
+ class Assignment(Primitive):
51
+ variable: Variable
52
+ expression: Expression
53
+
54
+
55
+ @dataclass
56
+ class Emit(Primitive):
57
+ variable: Variable
58
+
59
+
60
+ @dataclass
61
+ class Where(Primitive):
62
+ condition: Variable
63
+ inverted: bool
64
+
65
+
66
+ @dataclass
67
+ class Merge(Primitive): ...
68
+
69
+
70
+ @dataclass
71
+ class Jump(Primitive):
72
+ label: Label
73
+
74
+
75
+ @dataclass
76
+ class Fork(Primitive):
77
+ variables: list[Variable]
78
+ expression: Expression
79
+
80
+
81
+ @dataclass
82
+ class Gather(Primitive):
83
+ aggregates: dict[Variable, Expression]
84
+ keys: list[Variable]
85
+
86
+
87
+ @dataclass
88
+ class IsSynced(Primitive):
89
+ variable: Variable
90
+ label: Label
91
+ keys: list[Variable]
92
+
93
+
94
+ Program = BaseProgram[Plan]
flummi/IR/__init__.py ADDED
File without changes
flummi/IR/common.py ADDED
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from ..library.errors import Location
4
+
5
+ __all__ = (
6
+ "Expression",
7
+ "Identifier",
8
+ "Label",
9
+ "Located",
10
+ "Program",
11
+ "Type",
12
+ "Variable",
13
+ )
14
+
15
+
16
+ @dataclass(kw_only=True, match_args=False, slots=True)
17
+ class Located:
18
+ location: Location = field(hash=False, compare=False, repr=False)
19
+
20
+
21
+ @dataclass(unsafe_hash=True, order=True, slots=True)
22
+ class Identifier(Located):
23
+ identifier: str
24
+
25
+
26
+ Variable = Identifier
27
+ Label = Identifier
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class Expression(Located):
32
+ source: str
33
+ arguments: list[Variable]
34
+
35
+
36
+ @dataclass(unsafe_hash=True, order=True, slots=True)
37
+ class Type(Located):
38
+ source: str
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class Program[B](Located):
43
+ body: B
File without changes
@@ -0,0 +1,28 @@
1
+ from textwrap import dedent
2
+
3
+ from .. import CFP
4
+ from ...library import utils
5
+
6
+
7
+ def pretty(primitive: CFP.Primitive) -> str:
8
+ match primitive:
9
+ case CFP.Emit(variable):
10
+ return f"EMIT {variable.identifier}"
11
+
12
+ case CFP.Assignment(variable, expression):
13
+ this = f"LET {variable.identifier} = "
14
+ this += utils.indent1(
15
+ dedent(
16
+ expression.source.format(
17
+ *(
18
+ f"{{{argument.identifier}}}"
19
+ for argument in expression.arguments
20
+ )
21
+ )
22
+ ),
23
+ " " * len(this),
24
+ )
25
+ return this
26
+
27
+ case _:
28
+ return type(primitive).__name__.upper()
@@ -0,0 +1,68 @@
1
+ from .. import CFP
2
+ from .pretty import pretty
3
+
4
+
5
+ __all__ = ("render",)
6
+
7
+
8
+ PRORGAM_TEMPLATE = """\
9
+ digraph "program" {{
10
+ node [
11
+ shape = Mrecord,
12
+ nojustify = true,
13
+ fontname = "{font}",
14
+ ];
15
+ graph [
16
+ fontname = "{font}",
17
+ ];
18
+ edge [
19
+ fontname = "{font}",
20
+ ];
21
+ {nodes}
22
+ {edges}
23
+ }}
24
+ """
25
+
26
+ NODE_TEMPLATE = '"{label}" [label="{body}",{style}];'
27
+
28
+ NODE_STYLES: dict[type[CFP.Primitive], str] = {
29
+ CFP.Assignment: "style = filled, fillcolor = royalblue, color = white, fontcolor = white",
30
+ CFP.Emit: "style = filled, fillcolor = navy, color = white, fontcolor = white",
31
+ }
32
+
33
+
34
+ def render[A](graph: CFP.Plan, *, font: str = "monospace") -> str:
35
+ nodes = (
36
+ NODE_TEMPLATE.format(
37
+ label=label.identifier,
38
+ body=(
39
+ f"{{{
40
+ pretty(node)
41
+ .replace('\n', '\\l')
42
+ .replace('|', '\\|')
43
+ .replace(' ', '\\ ')
44
+ .replace('{', '\\{')
45
+ .replace('}', '\\}')
46
+ .replace('<', '\\<')
47
+ .replace('>', '\\>')
48
+ .replace('"', '\\"')
49
+ }\\l|{{{label.identifier}\\l|@{label.location.column}:{
50
+ label.location.line
51
+ }}}}}"
52
+ ),
53
+ style=NODE_STYLES.get(type(node), ""),
54
+ )
55
+ for label, node in graph.primitives.items()
56
+ )
57
+
58
+ edges: list[str] = []
59
+
60
+ edges.extend(
61
+ f'"{source.identifier}":s -> "{target.identifier}":n;'
62
+ for source, targets in graph.successors_of.items()
63
+ for target in targets
64
+ )
65
+
66
+ return PRORGAM_TEMPLATE.format(
67
+ nodes="\n".join(nodes), edges="\n".join(edges), font=font
68
+ )
flummi/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .compiler import compile
2
+
3
+ __all__ = ("compile",)
flummi/cli.py ADDED
@@ -0,0 +1,91 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from .compiler.allocation import allocate
9
+ from .compiler.analysis import analyze
10
+ from .compiler.generation import generate
11
+ from .compiler.lowering import lower
12
+ from .compiler.parsing import parse
13
+ from .compiler.solving import solve
14
+ from .IR.CFP import Plan
15
+ from .IR.pretty.render import render
16
+ from .library.errors import PrettyError
17
+
18
+ __all__ = ("cli",)
19
+
20
+ cli = typer.Typer()
21
+
22
+
23
+ @cli.command("compile")
24
+ def compile(
25
+ input: Annotated[
26
+ Path,
27
+ typer.Argument(
28
+ dir_okay=False,
29
+ file_okay=True,
30
+ readable=True,
31
+ exists=True,
32
+ help="The file to compile.",
33
+ ),
34
+ ],
35
+ output: Annotated[
36
+ Path | None,
37
+ typer.Argument(
38
+ dir_okay=False,
39
+ file_okay=True,
40
+ writable=True,
41
+ help="The path to write the compiled query to.",
42
+ ),
43
+ ] = None,
44
+ graph: Annotated[
45
+ Path | None,
46
+ typer.Option(
47
+ dir_okay=False,
48
+ file_okay=True,
49
+ writable=True,
50
+ help="File to render CFP to.",
51
+ ),
52
+ ] = None,
53
+ dot: Annotated[
54
+ str, typer.Option(help="GraphVis dot-command to use for rendering.")
55
+ ] = "dot",
56
+ ):
57
+ with open(input, "r") as f:
58
+ source = f.read()
59
+
60
+ try:
61
+ program = parse(source)
62
+
63
+ analysis = analyze(program)
64
+
65
+ lowered_program = lower(program)
66
+
67
+ if graph is not None:
68
+ render_to_file(lowered_program.body, graph, dot)
69
+
70
+ dataflow = solve(lowered_program, analysis)
71
+
72
+ allocation = allocate(lowered_program, analysis, dataflow)
73
+
74
+ sql = generate(lowered_program, analysis, dataflow, allocation)
75
+
76
+ except PrettyError as e:
77
+ print(e.format(source), file=sys.stderr)
78
+ sys.exit(1)
79
+
80
+ if output:
81
+ with open(output, "w+") as f:
82
+ _ = f.write(sql)
83
+ else:
84
+ print(sql)
85
+
86
+
87
+ def render_to_file(graph: Plan, path: Path, command: str = "dot"):
88
+ _ = subprocess.run(
89
+ args=[command, "-T", path.suffix[1:] or "png", "-o", path.absolute()],
90
+ input=render(graph).encode(),
91
+ )
@@ -0,0 +1,26 @@
1
+ from ..library.sql import SQL
2
+
3
+ from .parsing import parse
4
+ from .analysis import analyze
5
+ from .lowering import lower
6
+ from .solving import solve
7
+ from .allocation import allocate
8
+ from .generation import generate
9
+
10
+ __all__ = ("compile",)
11
+
12
+
13
+ def compile(source: str) -> SQL:
14
+ program = parse(source)
15
+
16
+ analysis = analyze(program)
17
+
18
+ lowered_program = lower(program)
19
+
20
+ dataflow = solve(lowered_program, analysis)
21
+
22
+ allocation = allocate(lowered_program, analysis, dataflow)
23
+
24
+ sql = generate(lowered_program, analysis, dataflow, allocation)
25
+
26
+ return sql
@@ -0,0 +1,155 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass, field
3
+
4
+ from ..IR.CFP import (
5
+ Label,
6
+ Program,
7
+ Start,
8
+ Variable,
9
+ )
10
+ from ..IR.common import Type
11
+ from ..library import utils
12
+ from .analysis import AnalysisResult
13
+ from .names import PROGRAM_VARIABLE, SystemVariable
14
+ from .solving import DataflowResult
15
+
16
+ __all__ = ("allocate",)
17
+
18
+
19
+ type Column = str
20
+ type Schema = dict[Column, Type]
21
+
22
+ type PerLabel[T] = dict[Label, T]
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class Allocation:
27
+ _variable_allocation: dict[Variable, Column]
28
+ _column_allocation: dict[Column, Variable] = field(init=False)
29
+
30
+ def __post_init__(self):
31
+ self._column_allocation = {
32
+ column: variable
33
+ for variable, column in self._variable_allocation.items()
34
+ }
35
+
36
+ def column_for(self, variable: Variable) -> Column | None:
37
+ return self._variable_allocation.get(variable)
38
+
39
+ def variable_at(self, column: Column) -> Variable | None:
40
+ return self._column_allocation.get(column)
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class AllocationResult:
45
+ schema: Schema
46
+ at: PerLabel[Allocation]
47
+
48
+
49
+ def allocate(
50
+ program: Program,
51
+ analysis: AnalysisResult,
52
+ data_flow: DataflowResult,
53
+ naive: bool = True,
54
+ ) -> AllocationResult:
55
+ labels_to_allocate: list[Label] = [
56
+ label
57
+ for label, primitive in program.body.primitives.items()
58
+ if isinstance(primitive, Start)
59
+ ]
60
+
61
+ SYSTEM_SCHEMA = {
62
+ variable.identifier: analysis.symbol_table[variable]
63
+ for variable in analysis.system_variables.values()
64
+ if variable.identifier != SystemVariable.CONTROL
65
+ }
66
+
67
+ if naive:
68
+ schema = SYSTEM_SCHEMA
69
+ allocations = {}
70
+ for label in labels_to_allocate:
71
+ schema |= {
72
+ input.identifier: analysis.symbol_table[input]
73
+ for input in data_flow.inputs_of[label]
74
+ }
75
+ allocations[label] = Allocation(
76
+ {
77
+ analysis.system_variables[
78
+ SystemVariable.LABEL
79
+ ]: SystemVariable.LABEL,
80
+ }
81
+ | {
82
+ input: input.identifier
83
+ for input in data_flow.inputs_of[label]
84
+ }
85
+ )
86
+
87
+ return AllocationResult(schema, allocations)
88
+
89
+ # First, collect all "type siblings", i.e., groupings of like typed
90
+ # variables, at each label we wish to perform column allocation for.
91
+ # Since we are only looking at nodes containing start primitives, we
92
+ # compute these type siblings over the inputs of the nodes. In addition
93
+ # to the sets of type siblings themselves, we also collect the maximum
94
+ # cardinality of them by type.
95
+
96
+ type_siblings_at: PerLabel[dict[Type, list[Variable]]] = defaultdict(
97
+ lambda: defaultdict(list)
98
+ )
99
+ max_type_counts: dict[Type, int] = defaultdict(int)
100
+
101
+ for label in labels_to_allocate:
102
+ for type, variables in utils.groupby(
103
+ iter(
104
+ data_flow.inputs_of[label]
105
+ - set(analysis.system_variables.values())
106
+ ),
107
+ lambda variable: analysis.symbol_table[variable],
108
+ ):
109
+ variables = list(variables)
110
+ type_siblings_at[label][type] = variables
111
+ count = len(variables)
112
+ if count > max_type_counts[type]:
113
+ max_type_counts[type] = count
114
+
115
+ # Second, we compute the schema of the working table in two parts. The
116
+ # first part simply contains the system columns, e.g. the column for
117
+ # goto targets, the one for program results etc. The second part,
118
+ # contains the program columns, i.e., this set contains just enough
119
+ # columns of each type, such that all sets of type siblings computed
120
+ # above are covered.
121
+
122
+ schema: Schema = dict(SYSTEM_SCHEMA)
123
+ offset: int = 0
124
+ offset_of: dict[Type, int] = {}
125
+ for type, count in max_type_counts.items():
126
+ offset_of[type] = offset
127
+ schema.update(
128
+ {
129
+ PROGRAM_VARIABLE.format(idx=offset + i): type
130
+ for i in range(count)
131
+ }
132
+ )
133
+ offset += count
134
+
135
+ # Lastly, we compute the allocations for each label based on the schema
136
+ # we determined above. (The reverse mappings are generated automatically
137
+ # elsewhere, thus we can focus on simply computing on direction here.)
138
+
139
+ allocations: dict[Label, Allocation] = {
140
+ label: Allocation(
141
+ {
142
+ analysis.system_variables[
143
+ SystemVariable.LABEL
144
+ ]: SystemVariable.LABEL,
145
+ }
146
+ | {
147
+ variable: PROGRAM_VARIABLE.format(idx=offset_of[type] + i)
148
+ for type, variables in type_siblings_at.get(label, {}).items()
149
+ for i, variable in enumerate(variables)
150
+ }
151
+ )
152
+ for label in labels_to_allocate
153
+ }
154
+
155
+ return AllocationResult(schema, allocations)