Skip to content
Merged
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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **`export trait`:** traits can be `export`/`public` and imported across modules for `impl`.
- **`@policy(kind: "homeostasis")` PoC:** attribute form parses alongside legacy
`homeostasis_policy` (lint still prefers migration).
- **`@policy(kind: "homeostasis"|"attention")`:** attribute forms parse alongside legacy
`homeostasis_policy` / `attention_policy`; lint warns only on legacy keywords. Feature examples,
`gps_loss_full_stack` workflow, and cognitive docs use `@policy`.
- **Typed config/format idents:** `provider: mock` and `serialize(x, json)` accepted (strings still
work); unknown values rejected at check time.
- **Generics hardening:** empty `<>`, duplicate type params, `T: Bound`, and `where` rejected with
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ robot SafePatrol {
}
```

Policy blocks (`decision_tree`, `recovery_policy`, `continuity_policy`, `homeostasis_policy`) extend
Policy blocks (`decision_tree`, `recovery_policy`, `continuity_policy`, `@policy` homeostasis/attention) extend
this pattern — see [examples/features/](examples/features/) and [Spanda 101 lesson
11](docs/spanda-101/11-distributed-decisions.md).

Expand Down
3 changes: 3 additions & 0 deletions crates/spanda-ast/src/assurance_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ pub enum AttentionPolicyDecl {
AttentionPolicyDecl {
name: String,
rules: Vec<String>,
/// True when parsed from legacy `attention_policy` keyword (vs `@policy`).
#[serde(default)]
legacy_syntax: bool,
span: Span,
},
}
Expand Down
44 changes: 44 additions & 0 deletions crates/spanda-core/tests/export_trait_generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,47 @@ robot R {
}
}
}

#[test]
fn at_policy_attention_parses() {
// Description:
// At policy attention parses.
//
// Inputs:
// None.
//
// Outputs:
// None.
//
// Example:

let source = r#"
@policy(kind: "attention")
MissionFocus {
rule suppress_low_priority;
}

robot R {
actuator wheels: DifferentialDrive;
behavior run() { wheels.stop(); }
}
"#;
let tokens = tokenize(source).expect("tokenize @policy attention");
let program = parse(tokens).expect("parse @policy attention");
let Program::Program {
attention_policies, ..
} = program;
assert_eq!(attention_policies.len(), 1);
match &attention_policies[0] {
spanda_ast::assurance_decl::AttentionPolicyDecl::AttentionPolicyDecl {
name,
rules,
legacy_syntax,
..
} => {
assert_eq!(name, "MissionFocus");
assert_eq!(rules, &["suppress_low_priority".to_string()]);
assert!(!legacy_syntax);
}
}
}
72 changes: 63 additions & 9 deletions crates/spanda-lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,37 +384,49 @@ fn lint_library_shaped_decls(program: &Program, issues: &mut Vec<LintIssue>) {
..
} = program;

// Flag each homeostasis_policy as library-shaped surface.
// Flag each legacy homeostasis_policy as library-shaped surface.
for policy in homeostasis_policies {
let spanda_ast::assurance_decl::HomeostasisPolicyDecl::HomeostasisPolicyDecl {
name,
legacy_syntax,
span,
..
} = policy;

// Skip preferred `@policy(kind: "homeostasis")` forms.
if !legacy_syntax {
continue;
}
issues.push(LintIssue {
rule: "library-shaped-decl".into(),
message: format!(
"`homeostasis_policy {name}` is library-shaped syntax — see \
docs/language-surface-inventory.md (migration to @policy attrs planned; \
syntax remains supported)"
"`homeostasis_policy {name}` is library-shaped syntax — prefer \
`@policy(kind: \"homeostasis\")` (see docs/language-surface-inventory.md)"
),
line: span.start.line,
column: span.start.column,
severity: LintSeverity::Warning,
});
}

// Flag each attention_policy as library-shaped surface.
// Flag each legacy attention_policy as library-shaped surface.
for policy in attention_policies {
let spanda_ast::assurance_decl::AttentionPolicyDecl::AttentionPolicyDecl {
name, span, ..
name,
legacy_syntax,
span,
..
} = policy;

// Skip preferred `@policy(kind: "attention")` forms.
if !legacy_syntax {
continue;
}
issues.push(LintIssue {
rule: "library-shaped-decl".into(),
message: format!(
"`attention_policy {name}` is library-shaped syntax — see \
docs/language-surface-inventory.md (migration to @policy attrs planned; \
syntax remains supported)"
"`attention_policy {name}` is library-shaped syntax — prefer \
`@policy(kind: \"attention\")` (see docs/language-surface-inventory.md)"
),
line: span.start.line,
column: span.start.column,
Expand Down Expand Up @@ -733,6 +745,48 @@ robot R {
);
}

#[test]
fn at_policy_forms_skip_library_shaped_lint() {
// Preferred `@policy` attribute forms should not warn.
//
// Parameters:
// None.
//
// Returns:
// None.
//
// Options:
// None.
//
// Example:
// at_policy_forms_skip_library_shaped_lint();

let source = r#"
module demo;
@policy(kind: "homeostasis")
KeepAlive {
metric battery_pct;
}
@policy(kind: "attention")
Focus {
rule suppress_low_priority;
}
robot R {
actuator wheels: DifferentialDrive;
behavior b() { wheels.stop(); }
}
"#;
let report = lint(source).expect("lint should parse");
assert!(
!report
.issues
.iter()
.any(|i| i.rule == "library-shaped-decl"),
"preferred @policy forms should not warn, got {:?}",
report.issues
);
}

#[test]
fn detects_empty_test_block() {
// Description:
Expand Down
104 changes: 69 additions & 35 deletions crates/spanda-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ impl Parser {
} else if self.check(TokenType::Ident) && self.peek().lexeme == "resilience_policy" {
resilience_policies.push(self.parse_resilience_policy()?);
} else if self.check(TokenType::AtSign) {
homeostasis_policies.push(self.parse_at_policy_homeostasis()?);
self.parse_at_policy(&mut homeostasis_policies, &mut attention_policies)?;
} else if self.check(TokenType::Ident) && self.peek().lexeme == "homeostasis_policy" {
homeostasis_policies.push(self.parse_homeostasis_policy()?);
} else if self.check(TokenType::Ident) && self.peek().lexeme == "attention_policy" {
Expand Down Expand Up @@ -10045,24 +10045,27 @@ impl Parser {
})
}

fn parse_at_policy_homeostasis(
fn parse_at_policy(
&mut self,
) -> Result<spanda_ast::assurance_decl::HomeostasisPolicyDecl, SpandaError> {
// Parse `@policy(kind: "homeostasis") Name { metric …; }` (preferred form).
homeostasis_policies: &mut Vec<spanda_ast::assurance_decl::HomeostasisPolicyDecl>,
attention_policies: &mut Vec<spanda_ast::assurance_decl::AttentionPolicyDecl>,
) -> Result<(), SpandaError> {
// Parse `@policy(kind: "…") Name { … }` and push into the matching list.
//
// Parameters:
// None.
// - `homeostasis_policies` — destination for `kind: "homeostasis"`
// - `attention_policies` — destination for `kind: "attention"`
//
// Returns:
// A homeostasis policy decl with `legacy_syntax = false`.
// Ok when a supported kind was parsed; parse error otherwise.
//
// Options:
// Only `kind: "homeostasis"` is supported in this PoC.
// PoC kinds: `"homeostasis"` (metric body) and `"attention"` (rule body).
//
// Example:
// self.parse_at_policy_homeostasis()?
// self.parse_at_policy(&mut homeostasis, &mut attention)?;

use spanda_ast::assurance_decl::HomeostasisPolicyDecl;
use spanda_ast::assurance_decl::{AttentionPolicyDecl, HomeostasisPolicyDecl};
let start = self.expect(TokenType::AtSign, "Expected '@'")?;
self.expect(TokenType::Policy, "Expected 'policy' after '@'")?;
self.expect(TokenType::Lparen, "Expected '(' after @policy")?;
Expand All @@ -10078,38 +10081,68 @@ impl Parser {
let kind_val = self.expect(TokenType::String, "Expected policy kind string")?;
let kind = str_val(&kind_val);
self.expect(TokenType::Rparen, "Expected ')' after @policy args")?;
if kind != "homeostasis" {
return Err(SpandaError::Parse {
message: format!(
"Unsupported @policy kind '{kind}' (PoC supports only \"homeostasis\")"
),
line: kind_val.line,
column: kind_val.column,
});
}
let name = self.parse_label("Expected policy name after @policy(...)")?;
self.expect(TokenType::Lbrace, "Expected '{' after policy name")?;
let mut metrics = Vec::new();
while !self.check(TokenType::Rbrace) && !self.check(TokenType::Eof) {
if self.check(TokenType::Ident) && self.peek().lexeme == "metric" {
self.advance();
metrics.push(self.parse_label("Expected metric name")?);
self.expect(TokenType::Semicolon, "Expected ';' after metric")?;
} else {

// Dispatch body parsing by policy kind.
match kind.as_str() {
"homeostasis" => {
let mut metrics = Vec::new();
while !self.check(TokenType::Rbrace) && !self.check(TokenType::Eof) {
if self.check(TokenType::Ident) && self.peek().lexeme == "metric" {
self.advance();
metrics.push(self.parse_label("Expected metric name")?);
self.expect(TokenType::Semicolon, "Expected ';' after metric")?;
} else {
return Err(SpandaError::Parse {
message: "Expected metric in @policy homeostasis body".into(),
line: self.peek().line,
column: self.peek().column,
});
}
}
let end = self.expect(TokenType::Rbrace, "Expected '}' to close @policy body")?;
homeostasis_policies.push(HomeostasisPolicyDecl::HomeostasisPolicyDecl {
name,
metrics,
legacy_syntax: false,
span: self.span_from(&start, &end),
});
}
"attention" => {
let mut rules = Vec::new();
while !self.check(TokenType::Rbrace) && !self.check(TokenType::Eof) {
if self.check(TokenType::Ident) && self.peek().lexeme == "rule" {
self.advance();
rules.push(self.parse_label("Expected rule name")?);
self.expect(TokenType::Semicolon, "Expected ';' after rule")?;
} else {
return Err(SpandaError::Parse {
message: "Expected rule in @policy attention body".into(),
line: self.peek().line,
column: self.peek().column,
});
}
}
let end = self.expect(TokenType::Rbrace, "Expected '}' to close @policy body")?;
attention_policies.push(AttentionPolicyDecl::AttentionPolicyDecl {
name,
rules,
legacy_syntax: false,
span: self.span_from(&start, &end),
});
}
_ => {
return Err(SpandaError::Parse {
message: "Expected metric in @policy homeostasis body".into(),
line: self.peek().line,
column: self.peek().column,
message: format!(
"Unsupported @policy kind '{kind}' (PoC supports \"homeostasis\" and \"attention\")"
),
line: kind_val.line,
column: kind_val.column,
});
}
}
let end = self.expect(TokenType::Rbrace, "Expected '}' to close @policy body")?;
Ok(HomeostasisPolicyDecl::HomeostasisPolicyDecl {
name,
metrics,
legacy_syntax: false,
span: self.span_from(&start, &end),
})
Ok(())
}

fn parse_attention_policy(
Expand Down Expand Up @@ -10140,6 +10173,7 @@ impl Parser {
Ok(AttentionPolicyDecl::AttentionPolicyDecl {
name,
rules,
legacy_syntax: true,
span: self.span_from(&start, &end),
})
}
Expand Down
3 changes: 2 additions & 1 deletion docs/attention-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Populated during `enrich_entity_autonomy()` from health/readiness severity mappi
## Language

```spanda
attention_policy MissionFocus {
@policy(kind: "attention")
MissionFocus {
rule suppress_low_priority;
rule boost_critical_health;
}
Expand Down
3 changes: 2 additions & 1 deletion docs/attention-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Center dashboards.
## Language

```spanda
attention_policy MissionFocus {
@policy(kind: "attention")
MissionFocus {
rule suppress_low_priority;
rule boost_critical_health;
}
Expand Down
3 changes: 2 additions & 1 deletion docs/bio-inspired-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ fusion remains rule-based; adaptive recovery uses statistics, not ML. Reflex tra
**Smoke:** `./scripts/cognitive_resilience_smoke.sh` (CI Integration job `bio-inspired-autonomy`;
alias for `./scripts/bio_inspired_autonomy_smoke.sh`)

**Language:** `homeostasis_policy` and `attention_policy` declarations parse in `.sd` programs.
**Language:** `@policy(kind: "homeostasis")` and `@policy(kind: "attention")` declarations parse in
`.sd` programs (legacy `homeostasis_policy` / `attention_policy` still accepted with a lint).

---

Expand Down
6 changes: 4 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -715,12 +715,14 @@ spanda recovery confidence
Language policies (optional in `.sd` programs):

```spanda
homeostasis_policy PlatformStability {
@policy(kind: "homeostasis")
PlatformStability {
metric cpu_pct;
metric memory_pct;
}

attention_policy MissionFocus {
@policy(kind: "attention")
MissionFocus {
rule boost_critical_health;
}
```
Expand Down
3 changes: 2 additions & 1 deletion docs/homeostasis.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ quality, scheduler ticks, runtime load, deadline misses, provider failures.
## Language

```spanda
homeostasis_policy PlatformStability {
@policy(kind: "homeostasis")
PlatformStability {
metric cpu_pct;
metric memory_pct;
metric battery_pct;
Expand Down
Loading
Loading