Liking cljdoc? Tell your friends :D

ClojureElisp Refactoring Plan

Generated via SAA (Silence/Abstract/Act) Strategy Analysis Date: 2026-02-01 (Updated: 2026-02-02)

Executive Summary

This plan identifies architectural improvements for ClojureElisp based on Domain-Driven Design (DDD), SOLID principles, TDD readiness, CLARITY, and SLAP (Single Level of Abstraction Principle) analysis. The compiler is functional but has accumulated technical debt as features were added organically.

Current State (Updated 2026-02-02):

  • analyzer.clj: 1,460 LOC (50+ special forms, macro system, env management)
  • emitter.clj: 1,285 LOC (326 function mappings, type system)
  • core.clj: 213 LOC (public API, file handling)
  • runtime.el: 1,754 LOC (150+ runtime functions including transducers, sets)
  • Tests: ~5,620 LOC across 7 test files (292 tests, 1800+ assertions)

SCC Metrics: | Language | Files | Code LOC | Complexity | |----------|-------|----------|------------| | Clojure | 12 | 6,777 | 1,011 | | Elisp | 3 | 1,589 | 64 | | Total | 24 | 9,419 | 1,142 |


SILENCE Phase: Codebase Observations

File Structure

src/clojure_elisp/
├── analyzer.clj    # AST construction (largest file)
├── emitter.clj     # Elisp code generation
├── core.clj        # Public API
├── nrepl.clj       # nREPL middleware
└── repl.clj        # REPL server

resources/clojure-elisp/
└── clojure-elisp-runtime.el  # Elisp runtime library

test/clojure_elisp/
├── analyzer_test.clj
├── emitter_test.clj
├── runtime_test.clj
├── core_test.clj
├── nrepl_test.clj
├── repl_test.clj
└── examples_test.clj

Key Observations

  1. Monolithic Analyzer: analyzer.clj handles too many concerns:

    • Special form analysis (50+ forms)
    • Macro expansion system
    • Environment/locals tracking
    • Destructuring expansion
    • Source location tracking
    • Namespace resolution
  2. Flat Core Function Mapping: core-fn-mapping in emitter.clj is a single 300+ entry map mixing:

    • Arithmetic operators
    • Collection functions
    • String functions (clojure.string/*)
    • Set operations (clojure.set/*)
    • Emacs buffer operations
    • Process operations
  3. Runtime Coupling: Runtime.el replicates Clojure semantics but has no contract with the emitter (implicit protocol).

  4. Test Coverage Pattern: Heavy happy-path testing, minimal error-path or property-based tests.


ABSTRACT Phase: Design Analysis

1. Domain-Driven Design (DDD) Lens

Bounded Contexts Identified

ContextResponsibilityCurrent Location
ReaderParse Clojure formsClojure's built-in reader
AnalyzerAST construction, macro expansionanalyzer.clj
EmitterCode generationemitter.clj
RuntimeElisp runtime supportclojure-elisp-runtime.el
ToolingREPL, nREPL integrationnrepl.clj, repl.clj

Domain Entities

  • AST Node: {:op keyword, :env map, ...} - Well-defined
  • Environment: {:ns, :locals, :aliases, :refers} - Implicit, mutable via dynamic vars
  • Macro Registry: Global atom - Hidden coupling

Missing DDD Concepts

  1. Aggregate Root: No clear aggregate for compilation unit
  2. Domain Events: No compile-time event system for plugins/extensions
  3. Value Objects: AST nodes are close but lack validation
  4. Anti-Corruption Layer: No boundary between Clojure reader and our analyzer

DDD Recommendations

Priority: HIGH
- [ ] Extract Environment as explicit value object with clear transitions
- [ ] Define CompilationUnit aggregate containing ns metadata + forms
- [ ] Add AST validation at context boundaries

2. SOLID Principles Lens

Single Responsibility Principle (SRP)

Violations Found:

FileResponsibilitiesShould Be
analyzer.cljSpecial forms + Macros + Env + Destructuring4 modules
emitter.cljCode gen + Name mangling + Core mappings3 modules
core.cljAPI + File I/O + Dependency graph2 modules

SRP Recommendations:

Priority: HIGH
- [ ] Extract macro system to macros.clj
- [ ] Extract destructuring to destructure.clj
- [ ] Extract core-fn-mapping to mappings.clj (organized by category)
- [ ] Extract file handling to files.clj

Open/Closed Principle (OCP)

Violations Found:

  1. Adding new special forms requires modifying special-forms map in analyzer.clj
  2. Adding new Elisp mappings requires modifying core-fn-mapping map
  3. No plugin/extension mechanism

OCP Recommendations:

Priority: MEDIUM
- [ ] Create extensible registry for special forms
- [ ] Create category-based mapping modules with merge semantics
- [ ] Add analyze-form multimethod for user extensions

Liskov Substitution Principle (LSP)

Status: ✅ Good

Multimethod dispatch (emit-node, analyze) provides proper substitutability. AST nodes with :op keys are polymorphic.

Interface Segregation Principle (ISP)

Status: ✅ Good

Functions are small and focused. Public API in core.clj is minimal.

Dependency Inversion Principle (DIP)

Violations Found:

  1. Emitter directly depends on mangle-name implementation
  2. No abstraction over runtime functions (compile-time contract)
  3. Hard-coded Elisp target (no multi-target potential)

DIP Recommendations:

Priority: LOW (for now)
- [ ] Define TargetLanguage protocol for future backends
- [ ] Create RuntimeContract spec validating emitter/runtime alignment

3. TDD Readiness Lens

Current Test Architecture

Test FileTestsAssertionsCoverage Focus
analyzer_test.clj~100~600AST structure
emitter_test.clj~100~700Output strings
runtime_test.clj~80~500End-to-end in Emacs
core_test.clj~30~150Integration

Test Strengths

  • Comprehensive happy-path coverage
  • Good isolation between analyzer and emitter tests
  • End-to-end tests via Emacs subprocess

Test Gaps

  1. Missing Error Tests: No tests for malformed input handling
  2. Missing Property Tests: No generative testing for invariants
  3. Missing Contract Tests: No tests verifying emitter/runtime alignment
  4. Slow Feedback: End-to-end tests require Emacs subprocess

TDD Recommendations

Priority: HIGH
- [ ] Add error-path tests for analyzer (malformed forms)
- [ ] Add property-based tests for invariants:
      - Balanced parens in output
      - All AST nodes have :op
      - All locals are bound
- [ ] Add contract tests: emitter output matches runtime expectations
- [ ] Create fast unit-test-only profile

Priority: MEDIUM  
- [ ] Add mutation testing to validate test quality
- [ ] Create test fixtures for common AST patterns

4. CLAIRY Analysis

Note: CLAIRY is not a widely-known methodology. Interpreting as "Clarity" - code readability and maintainability analysis.

Clarity Issues Found

  1. Long Functions: analyze-defn is 70+ lines with nested conditionals
  2. Implicit State: *env* and *source-context* dynamic vars hide data flow
  3. Magic Strings: Elisp output built via string interpolation
  4. Naming: Some inconsistency (emit-node vs emit, analyze vs analyze-*)

Clarity Recommendations

Priority: MEDIUM
- [ ] Extract sub-functions from analyze-defn (handle-single-arity, handle-multi-arity)
- [ ] Add type hints/specs for public functions
- [ ] Create Elisp output builders instead of raw format strings
- [ ] Standardize naming: all special-form analyzers as analyze-<form>

5. SLAP (Single Level of Abstraction Principle) Analysis

SLAP: Each function should operate at a single level of abstraction. Don't mix high-level orchestration with low-level details.

SLAP Violations Found

analyzer.clj:

FunctionLinesViolation
analyze1344-1436Mixes type dispatch (high) with literal analysis (low)
analyze-defn130-198Mixes arity detection (high) with param processing (low)
process-fn-params410-457Mixes extraction (high) with destructure expansion (low)
parse-iteration-clauses987-1027Clean - single abstraction level
analyze-iteration-clauses1066-1074Clean - proper delegation

emitter.clj:

FunctionLinesViolation
emit-node :defn439-525Mixes arity dispatch (high) with binding formatting (low)
emit-node :defmethod715-739Mixes destructure handling (high) with string building (low)
emit-node :reify931-977Multiple abstraction levels in one method
emit-doseq-clauses583-612Clean - single reduce operation
emit-for-clauses632-670Clean - single reduce operation

runtime.el:

FunctionLinesViolation
clel-transduce1282-1302Mixes argument parsing (high) with reduction (low)
clel-partition-all1582-1614Mixes transducer/lazy-seq branching with implementation
clel-get47-61Acceptable - type dispatch is inherent

SLAP Recommendations

Priority: HIGH
- [ ] Extract `analyze` type dispatch into helper predicates
- [ ] Split `analyze-defn` into analyze-single-arity-defn + analyze-multi-arity-defn
- [ ] Extract `emit-node :defn` into emit-single-arity + emit-multi-arity + emit-variadic
- [ ] Create emit-binding-block helper for repeated binding formatting

Priority: MEDIUM
- [ ] Extract argument parsing from clel-transduce into clel--parse-transduce-args
- [ ] Create unified arity-dispatch pattern for dual-mode functions (transducer/lazy)

6. Cyclomatic Complexity Hotspots

Functions with highest complexity (manual analysis):

FileFunctionEst. ComplexityIssue
analyzer.cljanalyze~25Large cond with 15+ branches
analyzer.cljanalyze-defn~12Nested if/let/when
analyzer.cljexpand-map-destructuring~10Multiple cond-> branches
emitter.cljemit-node :defn~15cond + nested format
emitter.cljemit-node :defrecord~8Multiple for comprehensions
runtime.elclel-get~8Type dispatch cond
runtime.elclel-set-join~10Nested loops + conditionals

Complexity Reduction Strategy:

  1. Extract type-dispatch into predicates: const? local? invoke?
  2. Use multimethod dispatch instead of cond where > 5 branches
  3. Extract string-building helpers to reduce format complexity

ACT Phase: Refactoring Tasks

Phase 1: Foundation (Low Risk)

These changes are additive and don't break existing functionality.

IDTaskEffortRiskDependencies
R1.1Extract macro system to macros.cljMediumLowNone
R1.2Extract destructuring to destructure.cljMediumLowNone
R1.3Split core-fn-mapping into category modulesSmallLowNone
R1.4Add error-path tests for malformed inputSmallLowNone
R1.5Create test fixtures moduleSmallLowNone

Phase 2: Structure (Medium Risk)

These changes reorganize code while maintaining the same behavior.

IDTaskEffortRiskDependencies
R2.1Extract Environment as value objectMediumMediumR1.1
R2.2Create CompilationUnit aggregateMediumMediumR2.1
R2.3Add property-based tests for AST invariantsMediumLowR1.4
R2.4Split large functions (analyze-defn, emit-node :defn)MediumMediumNone
R2.5Create Elisp builder helpersSmallLowNone

Phase 3: Architecture (Higher Risk)

These changes affect the fundamental architecture.

IDTaskEffortRiskDependencies
R3.1Extensible special-forms registryLargeMediumR2.2
R3.2Runtime contract validationLargeMediumR2.2
R3.3Multi-target abstraction (TargetLanguage protocol)LargeHighR3.1, R3.2
R3.4AST validation at boundariesMediumMediumR2.1

Proposed New Module Structure

src/clojure_elisp/
├── analyzer/
│   ├── core.clj          # Main analyze function, dispatch
│   ├── special_forms.clj # All special form analyzers
│   ├── macros.clj        # Macro system (registry, expansion)
│   ├── destructure.clj   # Destructuring logic
│   └── env.clj           # Environment management
├── emitter/
│   ├── core.clj          # Main emit function, dispatch
│   ├── forms.clj         # Form-specific emitters
│   ├── mappings/
│   │   ├── arithmetic.clj
│   │   ├── collections.clj
│   │   ├── strings.clj
│   │   ├── emacs.clj     # Buffer/process operations
│   │   └── sets.clj
│   └── builders.clj      # Elisp output builders
├── compiler.clj          # Compilation pipeline, CompilationUnit
├── api.clj               # Public API (renamed from core.clj)
├── files.clj             # File I/O, dependency graph
├── nrepl.clj             # nREPL middleware
└── repl.clj              # REPL server

Migration Path

Keeping Tests Green

  1. Create new modules alongside old code - don't delete yet
  2. Redirect old functions to new locations with deprecation warnings
  3. Run full test suite after each move
  4. Only remove old code after all tests pass with new structure

Backward Compatibility

Public API functions in clojure-elisp.core should remain stable:

  • emit - Compile single form
  • emit-forms - Compile multiple forms
  • compile-string - Compile string input
  • compile-file - Compile file
  • compile-ns - Compile namespace
  • compile-project - Compile project

Internal namespaces can change freely.


Risk Assessment

ChangeRisk LevelMitigation
Extracting macros.cljLowIsolated subsystem with clear boundaries
Extracting destructure.cljLowPure functions, easily tested
Splitting core-fn-mappingLowJust reorganization, no logic change
Environment refactorMediumMany touch points, need comprehensive tests
Extensible registriesMediumAPI design decisions, may affect plugins
Multi-target abstractionHighLarge scope, defer until clear use case

Metrics to Track

Post-refactoring, monitor:

  1. File sizes - No file > 500 LOC
  2. Cyclomatic complexity - No function > 10
  3. Test coverage - Maintain > 80%
  4. Build time - Should not increase significantly
  5. New form addition - Measure lines of code needed

Immediate Next Steps

  1. R1.4: Add 5-10 error-path tests for malformed input
  2. R1.3: Split core-fn-mapping into 5 category files
  3. R1.1: Extract macro system to macros.clj
  4. R1.5: Create test fixtures for common patterns

These are low-risk, high-value improvements that establish patterns for the larger refactoring.


Appendix: Key Decisions

Decision 1: Keep analyzer/emitter separation

Rationale: The current separation is sound. Analyzer produces AST, emitter consumes it. This follows classic compiler architecture.

Decision 2: Don't over-abstract prematurely

Rationale: TargetLanguage protocol is interesting but there's no second target yet. Keep it simple until there's a real use case.

Decision 3: Prioritize test infrastructure

Rationale: Better tests enable safer refactoring. Invest in test fixtures, property tests, and error tests before large structural changes.

Decision 4: Preserve the multimethod pattern

Rationale: emit-node multimethod dispatching on :op is idiomatic and extensible. Keep this pattern.

Decision 5: Address SLAP violations before structural changes

Rationale: SLAP violations make code harder to test and maintain. Extracting helper functions (Phase 1) creates cleaner seams for later module extraction (Phase 2+).


Appendix B: Recent Feature Growth (2026-01-29 to 2026-02-01)

These recent additions increased codebase size and complexity:

FeatureFiles ModifiedLOC AddedComplexity Impact
clel-031: Buffer/Process Interopanalyzer, emitter+200+60 mappings, 6 special forms
clel-039: for list comprehensionanalyzer, emitter+100Multi-binding iteration logic
clel-042: clojure.string namespaceemitter, runtime+15018 string function mappings
clel-043: Transducersemitter, runtime+40020+ transducer factories
clel-044: clojure.set namespaceemitter, runtime+35018 set functions
clel-045: Multi-binding for/doseqanalyzer, emitter+100Clause parsing complexity

Observation: runtime.el grew from ~1200 to ~1750 LOC in one week. This validates the need for runtime modularization.


Appendix C: Code Quality Standards (from Memory)

These are the project's established code quality standards:

  1. TDD: Write tests first, implement to pass. All PRs must maintain 0 failures.
  2. DDD: Compiler stages are bounded contexts. Don't leak abstractions.
  3. SOLID: Single responsibility per function/namespace. Depend on abstractions.
  4. CLARITY: Code should be self-evident. Name things for what they are.
  5. SLAP: Each function operates at one level of abstraction.

Target Metrics:

  • No source file > 500 LOC
  • No function complexity > 10
  • Prefer multimethods for dispatch over nested conditionals

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close