# PCL — Prompt Composition Language

PCL is a small DSL for authoring, composing, and managing LLM prompts as modular, version-controlled artifacts. A `.pcl` file compiles to a plain text string ready to use as a system prompt with any LLM API.

# Why PCL?

I came up with PCL as a way to manage prompts when they get too large and unwieldy. Most agentic applications today involve persisting prompts as a bunch of text/markdown files across disparate locations which are then read at runtime before using them for an LLM inference request. Parts of the prompt are often static (rules, persona, tools) while some can be more dynamic (user messages, context). Managing multiple sets of prompt .md files, with different versions, becomes a real engineering challenge. Template engines like Jinja alleviate this problem somewhat but I wanted something that went beyond simple variable substitution and was specifically tailored for LLM applications.

With PCL you get in-built support for templating, composition, compile time checks etc. Think of prompts that same way as you do code. You define your prompts as a modular set of text files (`.pcl` files), then have the compiler compile and weave the prompts together into a single IR template, which can be rendered at runtime with variable substitution. 

pcl files are just like any other source- version controlled, with a well defined syntax.

### Author

Tanay Deshmukh (& vibe-coded with Claude)

---

## Features

- **Blocks** — define named fragments with `@block name:`, compose them with `@include`
- **Imports** — split prompts across files with `@import ./file.pcl [as ns]`
- **Variables** — `${var}` resolved at render time; `${var | default}` for fallbacks
- **Conditionals** — `@if variable:` / `@if not variable:` for truthiness-based branching
- **Raw blocks** — `@raw` / `@end` passes content through unmodified (no interpolation)
- **Comments** — lines starting with `#` are stripped from output
- **Frontmatter** — optional YAML metadata (`version`, `description`, arbitrary keys)

---

## Installation

Requires Python 3.11+. Recommend using [uv](https://github.com/astral-sh/uv) for environment management.

```bash
git clone <repo>
cd pcl
uv venv
source .venv/bin/activate
uv pip install -e ".[dev]"
```

Verify:

```bash
pcl --help
```

---

## CLI

### `pcl compile`

Compile a `.pcl` file and dump its intermediate representation — a tree of `TEXT`, `VAR`, and `IF` segments. Useful for inspecting template structure before rendering.

```bash
pcl compile examples/agent.pcl
```

Example output:

```
Metadata:
  version: 1.0
  description: Research assistant system prompt

Segments:
TEXT  'You are an expert research assistant...'
VAR   ${date}
IF    premium:
  TEXT  'You have access to the premium document index.'
IF    not premium:
  TEXT  'Upgrade to unlock the premium document index.'
VAR   ${query | no query provided}
```

Use `-o`/`--output` to write the compiled IR to a binary `.pclc` file instead of printing it. This enables compile-once-render-many workflows without re-parsing source files.

```bash
pcl compile examples/agent.pcl -o agent.pclc
# Compiled to agent.pclc
```

### `pcl render`

Render a `.pcl` or `.pclc` file with explicit variable values.

```bash
# From source
pcl render examples/agent.pcl \
  --var date=2026-02-28 \
  --var query="What is alignment?" \
  --var premium=true

# From pre-compiled .pclc (no re-parsing)
pcl render agent.pclc \
  --var date=2026-02-28 \
  --var query="What is alignment?" \
  --var premium=true
```

`--var` accepts `key=value`. Values `true` and `false` are coerced to booleans.

### `pcl check`

Validate a `.pcl` file without producing output. Exits `0` on success, `1` on error. Prints the error message and line number on failure.

```bash
pcl check examples/agent.pcl
```

### `pcl watch`

Recompile whenever the file changes. Accepts the same `--var` flags as `render`.

```bash
pcl watch examples/agent.pcl \
  --var date=2026-02-28 \
  --var premium=false
```

---

## Python API

```python
from pcl import compile, render, serialize, deserialize, CompiledTemplate
import cbor2

# compile() — produces a CompiledTemplate (IR) with metadata and segments
template = compile("examples/agent.pcl")
print(template.metadata)    # {"version": 1.0, "description": "..."}
print(template.segments)    # [str, VarRef, Conditional, ...]

# render() — resolves variables and conditionals, returns final string
prompt = render("examples/agent.pcl", variables={
    "date": "2026-02-28",
    "query": "What is alignment?",
    "premium": True,
})
print(prompt)

# compile once, render many — avoids re-parsing for each variable set
template = compile("examples/agent.pcl")
for query in queries:
    prompt = render(template, variables={"date": "2026-02-28", "query": query})

# serialize to .pclc — write compiled IR to disk
with open("agent.pclc", "wb") as f:
    f.write(cbor2.dumps(serialize(template)))

# deserialize from .pclc — reconstruct IR without re-parsing source
with open("agent.pclc", "rb") as f:
    template = deserialize(cbor2.loads(f.read()))
prompt = render(template, variables={"date": "2026-02-28", "query": "..."})
```

---

## Language Quick Reference

```pcl
---
version: 1.0
description: Optional YAML frontmatter — available as metadata, not in output
---

@import ./other.pcl
@import ./lib.pcl as lib

# This is a comment — stripped from output

@block intro:
    Defined here, emitted only when @include'd.

@include intro          # emit a local block
@include lib.greet      # emit a named block from an imported file
@include other          # emit the entire body of other.pcl

@if premium:
    Shown only when premium is truthy.

@if not premium:
    Shown only when premium is falsy or absent.

Hello ${name}!              # required variable — error if missing
Hello ${name | world}!      # variable with default

@raw
${not_interpolated}  @not_a_directive  # this is not a comment
@end

\@block  →  literal @block in output
\#       →  literal # in output
\${      →  literal ${ in output
```

---

## Examples

The `examples/` directory contains a multi-file prompt:

| File | Purpose |
|------|---------|
| `examples/agent.pcl` | Main agent prompt — imports persona and tools |
| `examples/persona.pcl` | Persona blocks (`researcher`, `brief`) |
| `examples/tools.pcl` | Tool description blocks (`search`, `browse`, `premium_index`) |

```bash
pcl render examples/agent.pcl \
  --var date=2026-02-28 \
  --var query="Explain quantum entanglement" \
  --var premium=false
```

---

## Tests

```bash
uv run pytest tests/ -v
```

With coverage:

```bash
uv run pytest tests/ --cov=pcl --cov-report=term-missing
```

---

## VS Code Extension

Syntax highlighting is in a separate repo: [`src/pcl-vscode`](https://github.com/cybergla/pcl-vscode).
