Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package and test runner
run: |
python -m pip install --upgrade pip
python -m pip install -e . pytest

- name: Run tests
run: python -m pytest
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
# threadlang
AI-native DSL for structured LLM workflows: context, constraints, steps, and typed outputs.

## Run locally

```bash
python -m pip install -e .
python -m pytest
```

## Hello example

```bash
thread run examples/hello.thread --inputs name=world
```
23 changes: 23 additions & 0 deletions docs/grammar.ebnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(* ThreadLang v0.1 minimal grammar *)

program = "thread", IDENT, "{", { block }, "}" ;

block = context_block
| emit_block
| placeholder_block ;

context_block = "context", "{", { IDENT, "=", STRING }, "}" ;

emit_block = "emit", "text", "{", expr, "}" ;

placeholder_block = ("inputs" | "rules" | "steps"), "{", opaque, "}" ;

expr = term, { "+", term } ;
term = STRING | variable_ref ;
variable_ref = ("context" | "inputs"), ".", IDENT ;

IDENT = letter, { letter | digit | "_" } ;
STRING = '"', { character }, '"' ;

(* Opaque placeholder content is accepted by the implementation using brace depth. *)
opaque = { ? any token with balanced braces ? } ;
66 changes: 66 additions & 0 deletions docs/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ThreadLang v0.1 Specification (Skeleton)

ThreadLang is a deterministic, traceable DSL for composing threaded prompts and outputs.
This document describes the **minimal v0.1 subset** implemented in this repository.

## Design goals

- **Deterministic parsing:** a source file has one unambiguous parse tree.
- **Deterministic runtime:** evaluation order and output are stable for the same source + inputs.
- **Traceability:** runtime emits structured events (`parse_ok`, `context_set`, `emit`).

## Program shape

A program starts with a single thread block:

```threadlang
thread Name {
...blocks...
}
```

Supported block families in v0.1:

- `context { ... }`
- `inputs { ... }` *(placeholder in v0.1 parser; parsed as opaque block)*
- `rules { ... }` *(placeholder in v0.1 parser; parsed as opaque block)*
- `steps { ... }` *(placeholder in v0.1 parser; parsed as opaque block)*
- `emit text { ... }`

## context block

The context block contains string assignments:

```threadlang
context {
greeting = "Hello"
}
```

These values are available via `context.<key>` inside expressions.

## emit block

v0.1 supports only `emit text { <expr> }`.

Expressions support:

- String literals: `"hello"`
- Variable references: `context.key` and `inputs.key`
- Concatenation: `<expr> + <expr>`

Example:

```threadlang
emit text { context.greeting + ", " + inputs.name + "!" }
```

## Runtime behavior

Given a parsed AST and an input dictionary, runtime:

1. Emits `parse_ok`.
2. Applies context assignments in order and emits `context_set` for each.
3. Evaluates emit blocks in order, concatenates emitted `text`, emits `emit` events.

Result: `{ output: string, trace: TraceEvent[] }`.
7 changes: 7 additions & 0 deletions examples/hello.thread
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
thread HelloWorld {
context {
greeting = "Hello"
}

emit text { context.greeting + ", " + inputs.name + "!" }
}
23 changes: 23 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "threadlang"
version = "0.1.0"
description = "ThreadLang v0.1 interpreter skeleton"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "ThreadLang Contributors" }]

[project.scripts]
thread = "threadlang.cli:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
6 changes: 6 additions & 0 deletions src/threadlang/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""ThreadLang v0.1 package."""

from .parser import parse_program
from .runtime import run_program

__all__ = ["parse_program", "run_program"]
43 changes: 43 additions & 0 deletions src/threadlang/ast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""AST nodes for the ThreadLang v0.1 grammar."""

from dataclasses import dataclass
from typing import List


@dataclass(frozen=True)
class Program:
thread_name: str
context: List["ContextAssign"]
emits: List["Emit"]


@dataclass(frozen=True)
class ContextAssign:
key: str
value: str


@dataclass(frozen=True)
class Emit:
target: str
expression: "Expr"


class Expr:
"""Marker base class for expressions."""


@dataclass(frozen=True)
class StringLiteral(Expr):
value: str


@dataclass(frozen=True)
class VariableRef(Expr):
scope: str
key: str


@dataclass(frozen=True)
class Concat(Expr):
parts: List[Expr]
47 changes: 47 additions & 0 deletions src/threadlang/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Command-line interface for ThreadLang."""

from __future__ import annotations

import argparse
from pathlib import Path
from typing import Dict

from .parser import parse_program
from .runtime import run_program


def main() -> None:
parser = argparse.ArgumentParser(prog="thread")
subparsers = parser.add_subparsers(dest="command", required=True)

run_parser = subparsers.add_parser("run", help="Run a .thread program")
run_parser.add_argument("file", type=Path, help="Path to .thread source file")
run_parser.add_argument(
"--inputs",
nargs="*",
default=[],
metavar="key=value",
help="Input bindings available via inputs.<key>",
)

args = parser.parse_args()

if args.command == "run":
source = args.file.read_text(encoding="utf-8")
program = parse_program(source)
result = run_program(program, inputs=_parse_inputs(args.inputs))
print(result.output)


def _parse_inputs(items: list[str]) -> Dict[str, str]:
parsed: Dict[str, str] = {}
for item in items:
if "=" not in item:
raise ValueError(f"Input must be key=value, got: {item!r}")
key, value = item.split("=", 1)
parsed[key] = value
return parsed


if __name__ == "__main__":
main()
Loading