Coupling & Cohesion: Structure is Speed
Software tools face a recurring tension between waiting to build fast executables and speeding up the workflow at the cost of the end result. Traditional approaches have forced developers to choose: aggressive optimization with long compilation cycles produces efficient code, while rapid compilation cycles often yield code bloat. We want to give the developer that choice at the point where it counts, and functional programming already imposes the structure a compiler can read to keep the choice practical to offer.
Our Composer compiler is designed to use one aspect of that structure through coupling and cohesion analysis integrated directly into our Program Semantic Graph. By reading the relationships between parts of a program, Composer is meant to assist the developer in deciding compilation boundaries that serve both development velocity and runtime performance.
Coupling and Cohesion in Software
Cohesion measures how closely related the responsibilities within a single module or component are to each other. High cohesion means that everything in a module works together toward a common purpose. In functional programming, this often manifests as functions that operate on the same data types or participate in the same data transformation pipeline.
Coupling measures how much one module depends on another. Low coupling means that modules can be understood and modified independently. High coupling means that changes in one module frequently require changes in another.
Consider a simple example from data processing. Functions that parse CSV files, validate data formats, and transform records into internal representations naturally exhibit high cohesion, since they all serve the purpose of data ingestion. Meanwhile, these data processing functions might have low coupling to the user interface code that displays results, so the two can be developed and tested independently.
The Compilation Challenge
Traditional compilers treat all code somewhat uniformly during compilation. A C++ compiler might compile each source file separately, then link everything together. This approach works, but it misses opportunities for optimization that arise from understanding program structure.
Modern languages with more sophisticated type systems and module systems provide richer information that compilers can exploit. However, most compilation strategies still focus primarily on local optimizations within functions or modest inter-procedural analysis within small scopes.
Functional programming languages offer something different: they encourage programming patterns that create natural organizational boundaries. Pure functions don’t have hidden dependencies. Immutable data structures create predictable sharing patterns. Higher-order functions create clear abstraction layers. These patterns are more than good software engineering practice: each is a hint about program structure a compiler can use in its optimization decisions.
Program Semantic Graph as Optimizer
Our original concept for the Program Semantic Graph was to create a “glue” layer between the symbolic and well-structured representations that Clef Compiler Services (CCS) produce. The goal of our Fidelity framework, and the reason for its name, is to preserve types through compilation. This precept spans everything from physics-aware calculations to memory layout for zero-copy mechanics between computation units. The “tree shaking” that eliminates unused code from library source means only the necessary code reaches compilation, which is a form of optimization in itself. Since we are not bound to .NET’s (or Java’s) assembly method of application component inclusion, we expect to build lean binaries. We intend the PS²G to be the result of that reachability analysis in the computation graph, which is then handed to MLIR’s front end.
But in that process we also found an opportunity to optimize the compilation process from a developer perspective. With all of this symbolic and semantic information, we can find “natural boundaries” in code that help shape how the compiler organizes units of compilation, and make the build-over-build ergonomics of working in our Fidelity framework better for the developer.
PS²G as Architectural Map
The Program Semantic Graph in Composer serves as more than an intermediate representation; it functions as an architectural map of your entire program. Beyond what each unit contains, it records which are exercised together and which can be recompiled independently.
graph TD
subgraph "Data Processing Module"
PARSER[CSV Parser]
VALIDATOR[Data Validator]
TRANSFORMER[Record Transformer]
end
subgraph "Analysis Module"
CALC[Statistical Calculator]
RISK[Risk Analyzer]
METRICS[Metrics Engine]
end
subgraph "Output Module"
FORMATTER[Report Formatter]
EXPORT[Data Exporter]
end
PARSER --> VALIDATOR
VALIDATOR --> TRANSFORMER
TRANSFORMER --> CALC
TRANSFORMER --> RISK
CALC --> METRICS
RISK --> METRICS
METRICS --> FORMATTER
TRANSFORMER --> EXPORT
Traditional compiler intermediate representations focus on control flow and data dependencies within relatively small scopes. The PS²G takes a broader view, maintaining semantic information about relationships between modules, types, functions, and data structures throughout the entire program. Because the graph spans the program, analysis can follow relationships across module and type boundaries instead of staying inside a single control-flow scope.
As we’re still in early design stages with this feature area, we envision that the PS²G represents these relationships through semantic units that align with garden-variety program organization:
type SemanticUnit =
| Module of FSharpEntity
| Namespace of string
| FunctionGroup of FSharpMemberOrFunctionOrValue list
| TypeCluster of FSharpEntity listAs the PS²G is constructed from Clef source code, it preserves the rich type information and functional programming patterns that exist in the original source. Module boundaries become first-class entities in the graph, and function composition chains appear as connected subgraphs. Data transformation pipelines form clear patterns that Composer can analyze and optimize as units.
Coupling Analysis
Our approach in the context of compilation rests on understanding how different parts of your program depend on each other, then using that information to decide compilation units and optimization strategies.
The analysis quantifies these relationships through coupling measurements that capture both strength and dependency types:
type Coupling = {
From: SemanticUnit
To: SemanticUnit
Strength: float // 0.0 to 1.0
Dependencies: SymbolRelation list
}Consider a typical Clef application that processes financial data. Coupling analysis reveals patterns that file-based organization might obscure:
graph LR
subgraph "Strong Coupling (0.8+)"
PARSE[Data Parser] -.->|0.9| VALIDATE[Data Validator]
CALC[Math Functions] -.->|0.8| RISK[Risk Analysis]
end
subgraph "Medium Coupling (0.3-0.7)"
VALIDATE -.->|0.5| TRANSFORM[Data Transform]
RISK -.->|0.4| REPORT[Report Engine]
end
subgraph "Weak Coupling (0.1-0.2)"
TRANSFORM -.->|0.2| REPORT
PARSE -.->|0.1| EXPORT[Data Export]
end
This coupling information becomes invaluable for making compilation decisions. Strongly coupled components benefit from being compiled together, enabling aggressive inter-procedural optimization, function inlining, and specialized code generation. Weakly coupled components can be compiled separately, enabling parallel compilation and incremental rebuilds when only some parts of the system change. Preventing the cumulative slow-down of whole-program compilation keeps day-to-day development workable as a codebase grows.
Fortunately MLIR lends itself to this type of approach. Components with strong coupling can be placed in the same MLIR module, where the intermediate representation can preserve high-level relationships while progressively lowering to efficient machine code. Components with weak coupling could be compiled to separate MLIR modules with well-defined interfaces, enabling independent optimization and caching strategies.
Cohesion as a Guide for Optimization
Cohesion analysis reveals which parts of a larger program naturally belong together and can be optimized as units. High cohesion indicates opportunities for aggressive optimization, while low cohesion suggests boundaries where more conservative approaches might be appropriate.
The analysis measures how closely related the responsibilities within a semantic unit are to each other:
type Cohesion = {
Unit: SemanticUnit
Score: float // 0.0 to 1.0
InternalRelations: int
ExternalRelations: int
}In functional programming, high cohesion often emerges naturally from data-driven design. Functions that operate on the same discriminated union type exhibit natural cohesion, since they understand the same data structures and often participate in the same pattern matching logic. Functions in a data transformation pipeline show cohesion through their shared purpose and sequential relationships.
The compiler can exploit this cohesion information in several ways. Highly cohesive function groups become candidates for aggressive inlining and specialization. For instance, memory layout decisions can co-locate data structures that are always used together. Cohesion analysis also guides the granularity of compilation units. Functions with high cohesion benefit from being compiled together because the compiler can see their interactions and optimize across function boundaries. Functions with low cohesion can be compiled separately without losing optimization opportunities.
MLIR Integration and Modular Compilation
The marriage of coupling and cohesion analysis with MLIR’s capabilities creates opportunities for compilation strategies that adapt to program structure. MLIR’s multi-level approach means that high-level relationships discovered through coupling analysis can be preserved and utilized throughout the compilation process.
The analysis results could directly inform MLIR module organization, creating compilation boundaries that align with program architecture:
graph TB
subgraph "Coupling/Cohesion Analysis Results"
HC1[High Cohesion Group A<br/>Parser + Validator]
HC2[High Cohesion Group B<br/>Math + Risk Analysis]
LC[Low Cohesion<br/>Report Generator]
ISO[Isolated<br/>Export Utilities]
end
subgraph "MLIR Module Generation"
MOD1[MLIR Module 1<br/>Data Processing]
MOD2[MLIR Module 2<br/>Analytics Core]
MOD3[MLIR Module 3<br/>Output Services]
MOD4[MLIR Module 4<br/>Utilities]
end
subgraph "LLVM Compilation Units"
LIB1[Optimized Library 1<br/>DataProc.o]
LIB2[Optimized Library 2<br/>Analytics.o]
LIB3[Cached Library 3<br/>Output.o]
LIB4[Cached Library 4<br/>Utils.o]
end
HC1 --> MOD1
HC2 --> MOD2
LC --> MOD3
ISO --> MOD4
MOD1 --> LIB1
MOD2 --> LIB2
MOD3 --> LIB3
MOD4 --> LIB4
When strongly coupled components are compiled into the same MLIR module, the compiler can maintain high-level information about their relationships even as it progressively lowers through different dialects. Function composition chains can be represented directly in high-level MLIR dialects, then optimized as units before being lowered to more traditional representations.
Preserving Verification
The progressive lowering through MLIR dialects also enables verification at multiple levels. High-level invariants derived from functional programming patterns can be verified at the functional dialect level, and memory-safety properties at the intermediate levels. Traditional optimizations then apply at the LLVM dialect level, with the higher-level properties already established and preserved.
Cache on Hand
This approach also enables more selective caching strategies. Compilation units identified through coupling and cohesion analysis can be cached independently. When only weakly coupled components change, strongly coupled components can reuse their cached compilation results. When interface boundaries identified through coupling analysis remain stable, downstream components can avoid recompilation entirely.
Beyond Hello World
These concepts might seem abstract when applied to simple programs, but they become important as applications grow in complexity. A moderate application, perhaps a command-line tool that processes files and generates reports, begins to show coupling patterns worth reading. File parsing logic couples strongly with data validation. Report formatting couples weakly with core processing logic. These patterns gain meaning in the day-to-day of a developer who has to produce work quickly. A compilation strategy that respects them carries a direct cost-and-time impact for the team.
The coupling and cohesion analysis in Composer intends to scale with program complexity. Simple programs benefit from the “tree shaking” of the PS²G process. Complex programs would benefit further from compilation strategies that would be impossible without understanding program structure.
A large application, perhaps a distributed actor system with multiple services and detailed error recovery, exhibits coupling and cohesion patterns that become central to managing compilation complexity. Without analysis of these patterns, compilation times become prohibitive and optimization opportunities are missed. A compilation strategy that reads them lets larger projects be approached with more confidence, and, as with many functional programs, makes refactoring a lower-risk step. Less technical debt carries through to efficiency, security, and a lower total cost of ownership.
The Architectural Advantage
Functional programming’s emphasis on clear abstractions, explicit dependencies, and structured data creates natural architectural boundaries that traditional imperative languages struggle to maintain. The same boundaries that help a person reason about the code hold information that tooling and build processes can automate against.
Because pure functions have no hidden side effects, the compiler can reason about their behavior directly. Immutable data structures make more sharing and caching strategies safe, and respected module boundaries let compilation units be optimized independently with confidence. Larger architectures become more manageable, and teams can move faster on them.
The natural structure that emerges from good functional programming practices becomes the foundation for efficient, scalable compilation strategies. It follows your program’s actual architecture rather than imposing a fixed compilation shape on it.
This approach changes how we think about compilation. Our Fidelity framework is designed to read a program’s architecture during the build and set its optimization strategy from that structure, rather than pushing all code through one generic set of passes. The tooling and visibility into that process are meant to keep the developer in control of the optimization at each step.
The Road Ahead
Even our simplest examples, the hello world programs that validate basic compilation infrastructure, are designed with this broader architectural vision in mind. The Program Semantic Graph construction, the coupling and cohesion analysis, and the MLIR integration strategies are all meant to scale from trivial examples to complex applications.
We treat this as a working relationship between language design and compilation, not one more compiler optimization. When the language encourages patterns that create natural program structure, and the compiler understands and leverages that structure to its advantage, the result is a development experience that provides both rapid iteration during development and efficient execution in production.
This is one place where an abstraction from category theory connects to something a developer feels day to day, the time between writing code and running it. We expect that as the coupling and cohesion analysis matures, the same structure that keeps a program readable to a person is the structure our compiler reads to keep builds fast. That is the direction we will keep building toward as the rest of the framework comes into place.