Skip to content

Commit 7b4e6cd

Browse files
Merge pull request #1 from hyperpolymath/claude/phronesis-network-grammar-BQTIM
Phronesis core grammar network configuration
2 parents 2f6fbf6 + dbc79e5 commit 7b4e6cd

24 files changed

+3408
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

lib/phronesis.ex

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# SPDX-FileCopyrightText: 2025 Phronesis Contributors
3+
4+
defmodule Phronesis do
5+
@moduledoc """
6+
Phronesis: A Policy Language for Network Configuration.
7+
8+
A minimal, decidable DSL for expressing network policies with:
9+
- Consensus-gated execution
10+
- Formal verification guarantees
11+
- Modular standard library
12+
13+
## Grammar Overview
14+
15+
The core grammar is ~40 lines of EBNF with 15 keywords:
16+
17+
POLICY <name>:
18+
<condition>
19+
THEN <action>
20+
PRIORITY: <int>
21+
EXPIRES: <datetime> | never
22+
CREATED_BY: <identifier>
23+
24+
## Quick Start
25+
26+
# Parse and execute a policy
27+
{:ok, ast} = Phronesis.parse(\"""
28+
POLICY block_invalid_routes:
29+
Std.RPKI.validate(route) == "invalid"
30+
THEN REJECT("Invalid RPKI")
31+
PRIORITY: 100
32+
EXPIRES: never
33+
CREATED_BY: security
34+
\""")
35+
36+
{:ok, result} = Phronesis.execute(ast, environment)
37+
38+
## Design Principles
39+
40+
- **Simple Syntax, Powerful Modules**: Core language is minimal;
41+
power comes from the Std.* module library
42+
- **Decidable**: No loops, guaranteed termination
43+
- **Consensus-Safe**: Actions require distributed agreement
44+
- **Auditable**: All executions logged immutably
45+
"""
46+
47+
alias Phronesis.{Lexer, Parser, Interpreter, State}
48+
49+
@doc """
50+
Parse Phronesis source code into an AST.
51+
52+
## Examples
53+
54+
iex> Phronesis.parse("CONST x = 42")
55+
{:ok, [{:const, "x", {:literal, :integer, 42}}]}
56+
57+
iex> Phronesis.parse("INVALID SYNTAX")
58+
{:error, {:parse_error, "expected POLICY, IMPORT, or CONST", 1, 1}}
59+
"""
60+
@spec parse(String.t()) :: {:ok, list()} | {:error, term()}
61+
def parse(source) when is_binary(source) do
62+
with {:ok, tokens} <- Lexer.tokenize(source),
63+
{:ok, ast} <- Parser.parse(tokens) do
64+
{:ok, ast}
65+
end
66+
end
67+
68+
@doc """
69+
Tokenize source code into a list of tokens.
70+
71+
Useful for debugging the lexer.
72+
"""
73+
@spec tokenize(String.t()) :: {:ok, list()} | {:error, term()}
74+
def tokenize(source) when is_binary(source) do
75+
Lexer.tokenize(source)
76+
end
77+
78+
@doc """
79+
Execute an AST with the given environment and state.
80+
81+
Returns `{:ok, new_state}` on success, `{:error, reason}` on failure.
82+
83+
## Consensus Requirement
84+
85+
Actions only execute if consensus is achieved (per operational semantics Rule 2).
86+
"""
87+
@spec execute(list(), State.t()) :: {:ok, State.t()} | {:error, term()}
88+
def execute(ast, %State{} = state) do
89+
Interpreter.execute(ast, state)
90+
end
91+
92+
@doc """
93+
Create a new execution state.
94+
95+
## Options
96+
97+
- `:consensus_threshold` - Required vote percentage (default: 0.51)
98+
- `:agents` - List of agent IDs for consensus voting
99+
"""
100+
@spec new_state(keyword()) :: State.t()
101+
def new_state(opts \\ []) do
102+
State.new(opts)
103+
end
104+
105+
@doc """
106+
Load and parse a policy file.
107+
"""
108+
@spec load_file(Path.t()) :: {:ok, list()} | {:error, term()}
109+
def load_file(path) do
110+
case File.read(path) do
111+
{:ok, source} -> parse(source)
112+
{:error, reason} -> {:error, {:file_error, reason}}
113+
end
114+
end
115+
end

lib/phronesis/application.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# SPDX-FileCopyrightText: 2025 Phronesis Contributors
3+
4+
defmodule Phronesis.Application do
5+
@moduledoc false
6+
7+
use Application
8+
9+
@impl true
10+
def start(_type, _args) do
11+
children = [
12+
# Registry for module lookups
13+
{Registry, keys: :unique, name: Phronesis.ModuleRegistry}
14+
]
15+
16+
opts = [strategy: :one_for_one, name: Phronesis.Supervisor]
17+
Supervisor.start_link(children, opts)
18+
end
19+
end

lib/phronesis/ast.ex

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# SPDX-FileCopyrightText: 2025 Phronesis Contributors
3+
4+
defmodule Phronesis.AST do
5+
@moduledoc """
6+
Abstract Syntax Tree node definitions for Phronesis.
7+
8+
The AST follows the grammar specification with these node types:
9+
10+
## Top-Level Declarations
11+
12+
- `{:policy, name, condition, action, metadata}` - Policy declaration
13+
- `{:import, module_path, alias}` - Import statement
14+
- `{:const, name, expression}` - Constant declaration
15+
16+
## Expressions
17+
18+
- `{:binary_op, op, left, right}` - Binary operations (+, -, *, /, AND, OR)
19+
- `{:unary_op, op, operand}` - Unary operations (NOT, -)
20+
- `{:comparison, op, left, right}` - Comparisons (==, !=, >, <, etc.)
21+
- `{:module_call, path, args}` - Module function call
22+
- `{:identifier, name}` - Variable reference
23+
- `{:literal, type, value}` - Literal values
24+
25+
## Actions
26+
27+
- `{:execute, function, args}` - Execute action
28+
- `{:report, message}` - Report action
29+
- `{:reject, reason}` - Reject action
30+
- `{:accept, reason}` - Accept action
31+
- `{:block, actions}` - Compound action (BEGIN...END)
32+
- `{:conditional, condition, then_action, else_action}` - IF/THEN/ELSE
33+
34+
## Metadata
35+
36+
- `%{priority: int, expires: :never | datetime, created_by: identifier}`
37+
"""
38+
39+
@type name :: String.t()
40+
@type module_path :: [name()]
41+
42+
@type literal_type :: :integer | :float | :string | :boolean | :ip_address | :datetime
43+
@type literal :: {:literal, literal_type(), any()}
44+
45+
@type comparison_op :: :eq | :neq | :gt | :gte | :lt | :lte | :in
46+
@type binary_op :: :add | :sub | :mul | :div | :and | :or
47+
@type unary_op :: :not | :neg
48+
49+
@type expression ::
50+
{:binary_op, binary_op(), expression(), expression()}
51+
| {:unary_op, unary_op(), expression()}
52+
| {:comparison, comparison_op(), expression(), expression()}
53+
| {:module_call, module_path(), [expression()]}
54+
| {:identifier, name()}
55+
| literal()
56+
57+
@type action ::
58+
{:execute, name(), [expression()]}
59+
| {:report, expression()}
60+
| {:reject, expression() | nil}
61+
| {:accept, expression() | nil}
62+
| {:block, [action()]}
63+
| {:conditional, expression(), action(), action() | nil}
64+
65+
@type metadata :: %{
66+
priority: non_neg_integer(),
67+
expires: :never | String.t(),
68+
created_by: name() | nil
69+
}
70+
71+
@type policy :: {:policy, name(), expression(), action(), metadata()}
72+
@type import_decl :: {:import, module_path(), name() | nil}
73+
@type const_decl :: {:const, name(), expression()}
74+
75+
@type declaration :: policy() | import_decl() | const_decl()
76+
@type program :: [declaration()]
77+
78+
@doc """
79+
Create a policy node.
80+
"""
81+
@spec policy(name(), expression(), action(), metadata()) :: policy()
82+
def policy(name, condition, action, metadata) do
83+
{:policy, name, condition, action, metadata}
84+
end
85+
86+
@doc """
87+
Create an import node.
88+
"""
89+
@spec import_decl(module_path(), name() | nil) :: import_decl()
90+
def import_decl(path, alias_name \\ nil) do
91+
{:import, path, alias_name}
92+
end
93+
94+
@doc """
95+
Create a constant declaration node.
96+
"""
97+
@spec const_decl(name(), expression()) :: const_decl()
98+
def const_decl(name, value) do
99+
{:const, name, value}
100+
end
101+
102+
@doc """
103+
Create a binary operation node.
104+
"""
105+
@spec binary_op(binary_op(), expression(), expression()) :: expression()
106+
def binary_op(op, left, right) do
107+
{:binary_op, op, left, right}
108+
end
109+
110+
@doc """
111+
Create a comparison node.
112+
"""
113+
@spec comparison(comparison_op(), expression(), expression()) :: expression()
114+
def comparison(op, left, right) do
115+
{:comparison, op, left, right}
116+
end
117+
118+
@doc """
119+
Create a unary operation node.
120+
"""
121+
@spec unary_op(unary_op(), expression()) :: expression()
122+
def unary_op(op, operand) do
123+
{:unary_op, op, operand}
124+
end
125+
126+
@doc """
127+
Create a module call node.
128+
"""
129+
@spec module_call(module_path(), [expression()]) :: expression()
130+
def module_call(path, args) do
131+
{:module_call, path, args}
132+
end
133+
134+
@doc """
135+
Create an identifier node.
136+
"""
137+
@spec identifier(name()) :: expression()
138+
def identifier(name) do
139+
{:identifier, name}
140+
end
141+
142+
@doc """
143+
Create a literal node.
144+
"""
145+
@spec literal(literal_type(), any()) :: literal()
146+
def literal(type, value) do
147+
{:literal, type, value}
148+
end
149+
150+
@doc """
151+
Create an execute action node.
152+
"""
153+
@spec execute(name(), [expression()]) :: action()
154+
def execute(function, args \\ []) do
155+
{:execute, function, args}
156+
end
157+
158+
@doc """
159+
Create a report action node.
160+
"""
161+
@spec report(expression()) :: action()
162+
def report(message) do
163+
{:report, message}
164+
end
165+
166+
@doc """
167+
Create a reject action node.
168+
"""
169+
@spec reject(expression() | nil) :: action()
170+
def reject(reason \\ nil) do
171+
{:reject, reason}
172+
end
173+
174+
@doc """
175+
Create an accept action node.
176+
"""
177+
@spec accept(expression() | nil) :: action()
178+
def accept(reason \\ nil) do
179+
{:accept, reason}
180+
end
181+
182+
@doc """
183+
Create a block action node.
184+
"""
185+
@spec block([action()]) :: action()
186+
def block(actions) do
187+
{:block, actions}
188+
end
189+
190+
@doc """
191+
Create a conditional action node.
192+
"""
193+
@spec conditional(expression(), action(), action() | nil) :: action()
194+
def conditional(condition, then_action, else_action \\ nil) do
195+
{:conditional, condition, then_action, else_action}
196+
end
197+
198+
@doc """
199+
Create policy metadata.
200+
"""
201+
@spec metadata(keyword()) :: metadata()
202+
def metadata(opts \\ []) do
203+
%{
204+
priority: Keyword.get(opts, :priority, 0),
205+
expires: Keyword.get(opts, :expires, :never),
206+
created_by: Keyword.get(opts, :created_by)
207+
}
208+
end
209+
end

0 commit comments

Comments
 (0)