Skip to content

Commit f79db4f

Browse files
hyperpolymathclaude
andcommitted
Grammar v0.4.0: Agent ergonomics — send-to, @ref, query_trace
Section 20 (Agent Ergonomics) — features designed by an LLM for LLMs: send-to syntax: `send message to target` alongside `send(target, message)`. Natural language order for agents; function-call order preserved for compat. Parser detects form and normalises argument order in AST. @ref annotations: `@ref("key")` — pointers from code to VeriSimDB entries. Agents don't need inline comments; they need queryable documentation. Code stays clean (fewer tokens), rationale lives in database. Collected in Program.annotations, no operational semantics. query_trace: `query_trace("branch_label", given_context)` — look up prior decisions in the trace database. Returns Data record if found, Unit if not. Harvard-safe (returns Data). Enables explicit trace memoization beyond the `cached` keyword. 176 tests (169 + 7 new). Zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e03abb4 commit f79db4f

10 files changed

Lines changed: 418 additions & 10 deletions

File tree

crates/oo7-core/src/ast.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ use serde::{Deserialize, Serialize};
1717
#[derive(Debug, Clone, Serialize, Deserialize)]
1818
pub struct Program {
1919
pub declarations: Vec<TopLevelDecl>,
20+
/// Collected @ref annotations (Section 20: Agent Ergonomics).
21+
/// These have no operational semantics; metadata for agent consumers.
22+
#[serde(default)]
23+
pub annotations: Vec<RefAnnotation>,
24+
}
25+
26+
/// Reference annotation: @ref("key") — points to VeriSimDB documentation entry.
27+
/// Has no operational semantics; metadata for agent consumers.
28+
/// Part of Section 20: Agent Ergonomics.
29+
#[derive(Debug, Clone, Serialize, Deserialize)]
30+
pub struct RefAnnotation {
31+
pub key: String,
2032
}
2133

2234
/// Top-level declarations.
@@ -377,6 +389,12 @@ pub enum ControlExpr {
377389
Record(Vec<(String, ControlExpr)>),
378390
/// List literal in control context
379391
List(Vec<ControlExpr>),
392+
/// query_trace("label", context) — look up prior decision in trace database.
393+
/// Section 20: Agent Ergonomics. Harvard-safe: returns Data.
394+
QueryTrace {
395+
label: String,
396+
context: Box<DataExpr>,
397+
},
380398
}
381399

382400
/// Either a single expression or a block of statements.

crates/oo7-core/src/eval.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,39 @@ impl Evaluator {
982982

983983
ControlExpr::Migrate { .. } => RtValue::Unit, // Migration semantics not yet implemented
984984

985+
ControlExpr::QueryTrace { label, context } => {
986+
// Section 20: Agent Ergonomics — look up prior decision in trace.
987+
// Evaluate the context (pure Data) for potential future matching.
988+
let _ctx_val = self.eval_data(context, env);
989+
990+
// Search the trace for a record matching the label.
991+
let matching = self.trace.records.iter().find(|r| r.branch_id == *label);
992+
match matching {
993+
Some(record) => {
994+
// Return the trace record as a Data record.
995+
let mut fields = HashMap::new();
996+
fields.insert(
997+
"branch_id".to_string(),
998+
RtValue::String(record.branch_id.clone()),
999+
);
1000+
fields.insert(
1001+
"chosen".to_string(),
1002+
RtValue::String(record.chosen.clone()),
1003+
);
1004+
fields.insert(
1005+
"reason".to_string(),
1006+
RtValue::String(record.reason.clone()),
1007+
);
1008+
fields.insert(
1009+
"timestamp".to_string(),
1010+
RtValue::String(record.timestamp.clone()),
1011+
);
1012+
RtValue::Record(fields)
1013+
}
1014+
None => RtValue::Unit, // No prior decision found.
1015+
}
1016+
}
1017+
9851018
ControlExpr::MethodCall(receiver_expr, method, args) => {
9861019
let receiver = self.eval_control_expr(receiver_expr, env);
9871020
let evaluated_args: Vec<RtValue> = args

crates/oo7-core/src/eval_tests.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,4 +1092,77 @@ mod tests {
10921092

10931093
assert_eq!(reply, RtValue::String("got:world".to_string()));
10941094
}
1095+
1096+
// ================================================================
1097+
// SECTION 20: AGENT ERGONOMICS — query_trace
1098+
// ================================================================
1099+
1100+
#[test]
1101+
fn eval_query_trace_miss() {
1102+
// query_trace with no prior matching trace record returns Unit.
1103+
let mut evaluator = Evaluator::new();
1104+
let env = Env::new();
1105+
1106+
let expr = ControlExpr::QueryTrace {
1107+
label: "nonexistent".to_string(),
1108+
context: Box::new(DataExpr::String("ctx".to_string())),
1109+
};
1110+
1111+
let result = evaluator.eval_control_expr(&expr, &env);
1112+
assert_eq!(result, RtValue::Unit);
1113+
}
1114+
1115+
#[test]
1116+
fn eval_query_trace_hit() {
1117+
// Make a traced branch decision, then query_trace finds it.
1118+
use crate::trace::TraceRecord;
1119+
use serde_json::Value as JsonValue;
1120+
1121+
let mut evaluator = Evaluator::new();
1122+
let env = Env::new();
1123+
1124+
// Manually inject a trace record (simulating a prior branch decision).
1125+
let record = TraceRecord {
1126+
branch_id: "test_branch".to_string(),
1127+
branch_options: vec!["left".to_string(), "right".to_string()],
1128+
given_contexts: JsonValue::Null,
1129+
chosen: "left".to_string(),
1130+
reason: "predicate match".to_string(),
1131+
trace_report: JsonValue::Null,
1132+
timestamp: "2026-03-24T00:00:00Z".to_string(),
1133+
agent_id: "evaluator".to_string(),
1134+
hermeneutic_context: JsonValue::Null,
1135+
};
1136+
evaluator.trace.append(record);
1137+
1138+
let expr = ControlExpr::QueryTrace {
1139+
label: "test_branch".to_string(),
1140+
context: Box::new(DataExpr::Int(42)),
1141+
};
1142+
1143+
let result = evaluator.eval_control_expr(&expr, &env);
1144+
1145+
// Should return a Record with branch_id, chosen, reason, timestamp.
1146+
match result {
1147+
RtValue::Record(fields) => {
1148+
assert_eq!(
1149+
fields.get("branch_id"),
1150+
Some(&RtValue::String("test_branch".to_string()))
1151+
);
1152+
assert_eq!(
1153+
fields.get("chosen"),
1154+
Some(&RtValue::String("left".to_string()))
1155+
);
1156+
assert_eq!(
1157+
fields.get("reason"),
1158+
Some(&RtValue::String("predicate match".to_string()))
1159+
);
1160+
assert_eq!(
1161+
fields.get("timestamp"),
1162+
Some(&RtValue::String("2026-03-24T00:00:00Z".to_string()))
1163+
);
1164+
}
1165+
other => panic!("Expected Record, got {:?}", other),
1166+
}
1167+
}
10951168
}

crates/oo7-core/src/grammar.pest

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
// Top-level
1616
// ============================================================
1717

18-
program = { SOI ~ top_level_decl* ~ EOI }
18+
program = { SOI ~ (annotation | top_level_decl)* ~ EOI }
19+
20+
// ============================================================
21+
// Annotations (Section 20: Agent Ergonomics)
22+
// ============================================================
23+
24+
annotation = { ref_annotation }
25+
26+
ref_annotation = { "@ref" ~ "(" ~ string_literal ~ ")" }
1927

2028
top_level_decl = {
2129
data_binding
@@ -103,7 +111,9 @@ control_stmt = {
103111
let_binding = { "linear"? ~ "let" ~ pattern ~ "=" ~ control_expr }
104112

105113
send_stmt = {
106-
"send_final" ~ "(" ~ control_expr ~ "," ~ control_expr ~ ")"
114+
"send_final" ~ control_expr ~ "to" ~ control_expr
115+
| "send" ~ control_expr ~ "to" ~ control_expr
116+
| "send_final" ~ "(" ~ control_expr ~ "," ~ control_expr ~ ")"
107117
| "send" ~ "(" ~ control_expr ~ "," ~ control_expr ~ ")"
108118
}
109119

@@ -140,7 +150,8 @@ control_call_or_access = {
140150
}
141151

142152
control_primary = {
143-
"migrate" ~ "(" ~ control_expr ~ "," ~ "from" ~ ":" ~ locale_expr ~ "," ~ "to" ~ ":" ~ locale_expr ~ ")"
153+
"query_trace" ~ "(" ~ string_literal ~ "," ~ data_expr ~ ")"
154+
| "migrate" ~ "(" ~ control_expr ~ "," ~ "from" ~ ":" ~ locale_expr ~ "," ~ "to" ~ ":" ~ locale_expr ~ ")"
144155
| "exchange" ~ "(" ~ control_expr ~ "," ~ control_expr ~ ")"
145156
| "receive" ~ "(" ~ ")"
146157
| float_literal
@@ -543,6 +554,7 @@ keyword = {
543554
| "one_for_one" | "one_for_all" | "rest_for_one"
544555
| "budget" | "speculative" | "cached" | "dispatch" | "hypatia" | "onnx" | "custom"
545556
| "omega" | "zero" | "batch"
557+
| "to" | "query_trace"
546558
}
547559

548560
integer_literal = @{ "-"? ~ ASCII_DIGIT+ }

crates/oo7-core/src/parser.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,41 @@ pub fn parse_program(input: &str) -> Result<Program, ParseError> {
5454
/// Build a `Program` from the top-level `program` rule.
5555
fn build_program(pair: pest::iterators::Pair<Rule>) -> Program {
5656
let mut declarations = Vec::new();
57+
let mut annotations = Vec::new();
5758
for inner in pair.into_inner() {
5859
match inner.as_rule() {
5960
Rule::top_level_decl => {
6061
if let Some(decl) = build_top_level_decl(inner) {
6162
declarations.push(decl);
6263
}
6364
}
65+
Rule::annotation => {
66+
if let Some(ann) = build_annotation(inner) {
67+
annotations.push(ann);
68+
}
69+
}
6470
Rule::EOI => {}
6571
_ => {}
6672
}
6773
}
68-
Program { declarations }
74+
Program {
75+
declarations,
76+
annotations,
77+
}
78+
}
79+
80+
/// Build an annotation from the `annotation` rule.
81+
/// Currently only supports @ref annotations.
82+
fn build_annotation(pair: pest::iterators::Pair<Rule>) -> Option<RefAnnotation> {
83+
let inner = pair.into_inner().next()?;
84+
match inner.as_rule() {
85+
Rule::ref_annotation => {
86+
let str_pair = inner.into_inner().next()?;
87+
let key = parse_string_literal(str_pair.as_str());
88+
Some(RefAnnotation { key })
89+
}
90+
_ => None,
91+
}
6992
}
7093

7194
/// Build a single top-level declaration.
@@ -238,12 +261,36 @@ fn build_let_binding(pair: pest::iterators::Pair<Rule>) -> ControlStmt {
238261
}
239262

240263
/// Build a `send` or `send_final` statement.
264+
///
265+
/// Two forms are supported (Section 20: Agent Ergonomics):
266+
/// - Natural-language: `send message to target` / `send_final message to target`
267+
/// (first expr is message, second is target)
268+
/// - Parenthesised: `send(target, message)` / `send_final(target, message)`
269+
/// (first expr is target, second is message)
270+
///
271+
/// The AST always stores target first, message second.
241272
fn build_send_stmt(pair: pest::iterators::Pair<Rule>) -> ControlStmt {
242273
let src = pair.as_str();
243274
let is_final = src.starts_with("send_final");
275+
276+
// Detect whether this is the `to` form or the parenthesised form.
277+
// The `to` form does NOT contain a '(' after the keyword.
278+
let keyword_len = if is_final { "send_final".len() } else { "send".len() };
279+
let after_keyword = src[keyword_len..].trim_start();
280+
let is_to_form = !after_keyword.starts_with('(');
281+
244282
let mut inner = pair.into_inner();
245-
let target = build_control_expr(inner.next().unwrap());
246-
let message = build_control_expr(inner.next().unwrap());
283+
let first = build_control_expr(inner.next().unwrap());
284+
let second = build_control_expr(inner.next().unwrap());
285+
286+
// In the `to` form: first=message, second=target → swap to AST order.
287+
// In the `()` form: first=target, second=message → already correct.
288+
let (target, message) = if is_to_form {
289+
(second, first)
290+
} else {
291+
(first, second)
292+
};
293+
247294
if is_final {
248295
ControlStmt::SendFinal { target, message }
249296
} else {
@@ -443,6 +490,17 @@ fn build_control_primary(pair: pest::iterators::Pair<Rule>) -> ControlExpr {
443490
let mut inner = pair.into_inner().peekable();
444491

445492
// Check for special forms by source text prefix
493+
if src.starts_with("query_trace") {
494+
// query_trace("label", data_expr)
495+
let str_pair = inner.next().unwrap();
496+
let label = parse_string_literal(str_pair.as_str());
497+
let data_pair = inner.next().unwrap();
498+
let context = build_data_expr(data_pair);
499+
return ControlExpr::QueryTrace {
500+
label,
501+
context: Box::new(context),
502+
};
503+
}
446504
if src.starts_with("migrate") {
447505
// migrate(expr, from: locale, to: locale)
448506
let expr = build_control_expr(inner.next().unwrap());

0 commit comments

Comments
 (0)