Skip to content

Improve macro compile-time diagnostics and key resolution validation#73

Open
adiman9 wants to merge 8 commits intomainfrom
compiler-errors
Open

Improve macro compile-time diagnostics and key resolution validation#73
adiman9 wants to merge 8 commits intomainfrom
compiler-errors

Conversation

@adiman9
Copy link
Contributor

@adiman9 adiman9 commented Mar 22, 2026

Summary

  • tighten the hyperstack-macros validation pipeline so attribute parsing preserves source spans, validates arguments eagerly, and reports actionable compile-time errors instead of opaque expansion failures
  • add IDL-aware validation and diagnostic collection for malformed #[map], #[resolve], #[aggregate], #[event], #[view], and top-level macro arguments, plus resolver and computed-field validation paths
  • enforce handler key-resolution rules during macro expansion for account sources, instruction handlers, event handlers, and #[derive_from] hooks so entities must prove a path back to their primary key
  • update the interpreter compiler to treat lookup indexes ending in _address as aliases for the underlying lookup field when resolving handlers
  • expand regression coverage with dynamic compile-failure tests and UI fixtures for the new diagnostics, including dedicated coverage for key-resolution validation failures

What Changed

  • introduced shared diagnostic plumbing in hyperstack-macros to accumulate and surface multiple validation errors with source-aware spans
  • refactored attribute parsing to validate strategy values, conditions, field paths, resolver conditions, URL templates, and resolver hook declarations as part of macro expansion
  • added validation passes for IDL references, computed cycles, invalid view sort fields, malformed resolver configuration, missing proto inputs, and unsupported top-level arguments
  • added a dedicated key-resolution validation pass that checks whether each handler can resolve the entity primary key via explicit primary-key mappings, lookup indexes, joins, lookup_by, or resolver hooks
  • refreshed the generated Ore SDK artifacts and kept the follow-up runtime formatting changes that came from touching related code paths during this work

Commit Breakdown

  • fix: surface hyperstack macro validation failures during expansion
  • chore: refresh ore generated stack artifacts
  • chore: format touched runtime crates
  • fix: validate handler key resolution paths during macro expansion

Reviewer Notes

  • the substantive review is in bb6d515 and 8f23f4c; the other two commits are generated artifact refreshes and formatting follow-up
  • this branch was rewritten to remove accidentally committed planning docs and IDL fixtures, so the PR now reflects only the compiler and runtime changes still intended for merge
  • reviewing commit-by-commit is likely the easiest path because the validation work touches parser, entity processing, codegen-adjacent plumbing, and test fixtures at once

Testing

  • added new dynamic and UI regression coverage in hyperstack-macros/tests/

@vercel
Copy link

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperstack-docs Ready Ready Preview, Comment Mar 22, 2026 2:42pm

Request Review

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adiman9 has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@adiman9 adiman9 changed the title Add AST versioning and improve macro compile-time diagnostics Improve macro compile-time diagnostics and key resolution validation Mar 22, 2026
@greptile-apps
Copy link

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR wires up a comprehensive compile-time diagnostic and validation pipeline for hyperstack-macros, replacing silent expansion failures and opaque panic!/expect calls with span-aware syn::Error propagation and actionable error messages.

Key changes:

  • diagnostic.rs — new shared ErrorCollector, fuzzy-match suggestion helpers, and parse_generated_items; all error paths now accumulate and emit multiple errors rather than aborting on the first.
  • parse/attributes.rs — all attribute structs gain source spans; validate_strategy unifies strategy validation; condition literals are eagerly parsed at parse time; parse_recognized_field_attribute replaces scattered if let Ok(Some(...)) chains with a single typed dispatch.
  • parse/conditions.rsparse_resolver_condition_expression now uses find_top_level_operator (consistent with the #[map] path), tries i64 before f64, and rejects non-finite floats — all three regressions from the previous review round are closed.
  • validation/mod.rs — new 1,028-line validation module with passes for key-resolution (account sources, instruction handlers, event handlers, #[derive_from] hooks), IDL field references, computed-field dependency cycles, view sort fields, and resolver from/schedule_at fields. Groups are sorted before first() is sampled, eliminating the span-nondeterminism concern from the previous review.
  • interpreter/src/compiler.rs — lookup indexes named mint_address are now reachable via a lookup_by = mint handler, consistent with the lookup_index_leafs alias logic in the macro validator.
  • stream_spec/module.rs.hyperstack/ write failure is correctly downgraded back to a non-fatal eprintln! warning.
  • Tests — dynamic compile-failure tests and UI fixtures for all new diagnostic paths, plus dedicated key-resolution regression coverage.

Issues found:

  • parse_url_template does not reject an empty field reference ({} or { }), silently producing UrlTemplatePart::FieldRef("") which fails at runtime.
  • ValidatedFieldPath.span captures only the first identifier of multi-segment paths (e.g. stats.created_at points to stats), making schedule_at diagnostics misleading.
  • validate_event_references gates the join_on entity-field check behind a successful IDL lookup; since join_on references entity fields (not IDL fields), the check should run independently of IDL availability.

Confidence Score: 3/5

  • Safe to merge with minor issues — two are low-severity diagnostic UX gaps and one is a logic gap in event-handler validation that allows a misspelled join_on to go undetected when IDL lookup fails.
  • The validation logic is well-structured, most issues from the previous review round have been addressed (conditions.rs integer precision, resolver condition operator parsing, diagnostic deduplication, span nondeterminism via group sorting, debug_assert for cycle invariant, module.rs non-fatal file-write). Three new issues remain: an empty field-ref gap in URL template parsing that causes silent runtime failures, a first-segment-only span in ValidatedFieldPath affecting schedule_at diagnostics, and join_on entity-field validation being incorrectly gated behind IDL availability in validate_event_references. None of these are blocking correctness for the common case, but the URL template gap can produce silent runtime errors.
  • Pay close attention to hyperstack-macros/src/stream_spec/entity.rs (empty {} in URL templates), hyperstack-macros/src/parse/attributes.rs (ValidatedFieldPath first-segment span), and hyperstack-macros/src/validation/mod.rs (validate_event_references join_on gating).

Important Files Changed

Filename Overview
hyperstack-macros/src/validation/mod.rs New 1,028-line validation module implementing key-resolution, IDL-reference, computed-cycle, view-sort, and resolver-field validation passes. Groups are sorted before first() is taken (addressing previous span-nondeterminism concerns), and debug_assert! guards the cycle-detection invariant. The join_on entity-field check inside validate_event_references is incorrectly gated behind a successful IDL lookup, allowing misspelled join_on targets to go undetected when the IDL is unavailable. The is_event_source guard at line 354 remains dead code (field is always false).
hyperstack-macros/src/parse/attributes.rs Attribute parsing significantly refactored: source spans are now preserved on all attribute structs, strategy validation is unified via validate_strategy, condition literals are eagerly parsed and typed, and parse_recognized_field_attribute provides a single dispatch point. ValidatedFieldPath.span captures only the first segment of multi-segment paths, producing misleading span targets in schedule_at diagnostics.
hyperstack-macros/src/stream_spec/entity.rs process_entity_struct and process_entity_struct_with_idl now return syn::Result, propagating parse errors instead of silently failing. parse_url_template correctly catches unclosed braces but does not validate that field references inside {} are non-empty, allowing UrlTemplatePart::FieldRef("") to be produced silently.
hyperstack-macros/src/diagnostic.rs New shared diagnostic plumbing: ErrorCollector, combine_errors, suggestion_or_available_suffix, invalid_choice_message, unknown_value_message. Well-tested with inline unit tests; the invalid_choice_message_does_not_repeat_available_values test explicitly guards the redundant-suffix regression. Clean and correct.
hyperstack-macros/src/parse/conditions.rs parse_resolver_condition_expression now uses find_top_level_operator (consistent with the #[map] path) and tries i64 before f64 (preserving precision for large integers). Non-finite floats explicitly return Err. All three regressions from the previous review round are addressed with dedicated unit tests.
interpreter/src/compiler.rs Adds _address suffix alias matching for lookup-index resolution: an index named mint_address is now reachable via a lookup_by = mint handler. The direction (index ends in _address, base matches handler field) is consistent with lookup_index_leafs in the validation module.
hyperstack-macros/src/validation/idl_refs.rs New IDL-reference validation helpers for instruction/account/type lookups with fuzzy-match suggestions. resolve_instruction_lookup_from_string falls back to idls.first() when no program-name match is found, which could produce false-positive "instruction not found" errors in multi-IDL configurations where the instruction lives in a non-first IDL.
hyperstack-macros/src/stream_spec/module.rs process_module now returns syn::Result and propagates errors from all sub-phases. The .hyperstack/ file write failure is correctly demoted back to an eprintln! warning (line 173), addressing the previous review concern about hard failures in read-only CI environments.
hyperstack-macros/tests/key_resolution_dynamic.rs Dynamic compile-failure tests that spawn temporary crates via cargo check. Coverage includes account sources, instruction handlers, event handlers, #[derive_from] hooks, and passing configurations. Test infrastructure is straightforward; tests correctly assert on stderr content fragments.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["#[hyperstack] macro invoked"] --> B["parse_proto_files_from_attr"]
    B --> C{"IDL files?"}
    C -- yes --> D["process_idl_spec"]
    C -- no --> E["Collect entity structs"]
    E --> F["parse_recognized_field_attribute\n(#[map] / #[event] / #[resolve] / ...)"]
    F --> G["Validate strategies, conditions,\nfield paths eagerly"]
    G --> H["process_entity_struct_with_idl"]
    H --> I["validate_semantics"]
    I --> J["validate_key_resolution_paths"]
    I --> K["validate_mapping_references\n(IDL field lookup)"]
    I --> L["validate_computed_fields\n(cycle detection)"]
    I --> M["validate_views\n(sort-field exists)"]
    I --> N["validate_resolve_specs\n(from/schedule_at fields exist)"]
    J --> J1["validate_source_handler_keys\n(account + instruction sources)"]
    J --> J2["validate_event_handler_keys"]
    J --> J3["validate_instruction_hook_keys\n(#[derive_from])"]
    H --> O["generate AST + embedded JSON spec"]
    O --> P{"write .hyperstack/ file"}
    P -- success --> Q["generate_multi_entity_builder"]
    P -- failure --> R["eprintln! warning\n(non-fatal)"]
    R --> Q
    Q --> S["Emit final TokenStream"]
Loading

Comments Outside Diff (1)

  1. hyperstack-macros/src/parse/attributes.rs, line 303-322 (link)

    P2 ValidatedFieldPath.span captures only the first segment

    parse_validated_field_path sets span = first.span() — the span of the leading identifier. For a multi-segment path such as stats.created_at, any downstream diagnostic (e.g. "unknown resolver schedule_at field 'stats.created_at'") will point only to stats, not to the full dotted path. Because proc_macro2 allows joining spans (first.span().join(last.span())), the full range can be captured with minimal additional effort:

    let mut segments = Vec::new();
    let first: syn::Ident = input.parse()?;
    let mut last_span = first.span();
    segments.push(first.to_string());
    
    while input.peek(Token![.]) {
        input.parse::<Token![.]>()?;
        let next: syn::Ident = input.parse()?;
        last_span = next.span();
        segments.push(next.to_string());
    }
    
    // join spans where the compiler supports it; fall back to first segment
    let span = first.span().join(last_span).unwrap_or(first.span());

    This produces a span that underlines the entire field path in the error output.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: hyperstack-macros/src/parse/attributes.rs
    Line: 303-322
    
    Comment:
    **`ValidatedFieldPath.span` captures only the first segment**
    
    `parse_validated_field_path` sets `span = first.span()` — the span of the leading identifier. For a multi-segment path such as `stats.created_at`, any downstream diagnostic (e.g. "unknown resolver `schedule_at` field 'stats.created_at'") will point only to `stats`, not to the full dotted path. Because `proc_macro2` allows joining spans (`first.span().join(last.span())`), the full range can be captured with minimal additional effort:
    
    ```rust
    let mut segments = Vec::new();
    let first: syn::Ident = input.parse()?;
    let mut last_span = first.span();
    segments.push(first.to_string());
    
    while input.peek(Token![.]) {
        input.parse::<Token![.]>()?;
        let next: syn::Ident = input.parse()?;
        last_span = next.span();
        segments.push(next.to_string());
    }
    
    // join spans where the compiler supports it; fall back to first segment
    let span = first.span().join(last_span).unwrap_or(first.span());
    ```
    
    This produces a span that underlines the entire field path in the error output.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: hyperstack-macros/src/stream_spec/entity.rs
Line: 65-83

Comment:
**Empty field reference in URL template not validated at compile time**

`parse_url_template` now correctly rejects unclosed braces (`{` without `}`). However, it does not validate that the captured `field_ref` is non-empty after trimming. A template such as `"https://api.example.com/{}/data"` or `"https://api.example.com/{   }/data"` will silently produce a `UrlTemplatePart::FieldRef("")`, which causes a runtime panic/error when the resolver attempts to look up an empty field path — with no compile-time warning.

Since the PR's intent is to surface these errors eagerly during macro expansion, this case is a natural extension of the same validation:

```rust
let field_ref = rest[open + 1..close].trim().to_string();
if field_ref.is_empty() {
    return Err(syn::Error::new(
        span,
        format!("Empty field reference '{{}}' in URL template: {s}"),
    ));
}
parts.push(UrlTemplatePart::FieldRef(field_ref));
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: hyperstack-macros/src/parse/attributes.rs
Line: 303-322

Comment:
**`ValidatedFieldPath.span` captures only the first segment**

`parse_validated_field_path` sets `span = first.span()` — the span of the leading identifier. For a multi-segment path such as `stats.created_at`, any downstream diagnostic (e.g. "unknown resolver `schedule_at` field 'stats.created_at'") will point only to `stats`, not to the full dotted path. Because `proc_macro2` allows joining spans (`first.span().join(last.span())`), the full range can be captured with minimal additional effort:

```rust
let mut segments = Vec::new();
let first: syn::Ident = input.parse()?;
let mut last_span = first.span();
segments.push(first.to_string());

while input.peek(Token![.]) {
    input.parse::<Token![.]>()?;
    let next: syn::Ident = input.parse()?;
    last_span = next.span();
    segments.push(next.to_string());
}

// join spans where the compiler supports it; fall back to first segment
let span = first.span().join(last_span).unwrap_or(first.span());
```

This produces a span that underlines the entire field path in the error output.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: hyperstack-macros/src/validation/mod.rs
Line: 660-713

Comment:
**`join_on` entity-field validation is gated behind a successful IDL lookup**

`validate_event_references` validates `event_attr.join_on` as a reference to a known entity field (lines 699–710). This check is only reached if the IDL lookup for the instruction succeeds — when it fails, the function `continue`s before reaching the `join_on` block.

`join_on` on an `#[event]` attribute names an **entity** field, not an instruction argument, so its validity is entirely independent of whether the IDL is available. A stale IDL or an unrecognised instruction path would suppress the entity-field check, masking a misspelled `join_on` target:

```rust
// validate join_on against entity fields regardless of IDL availability
if let Some(join_on) = &event_attr.join_on {
    let reference = join_on.ident.to_string();
    if !known_fields.contains(&reference) {
        errors.push(entity_field_error(
            entity_name, &reference, "join_on field",
            join_on.ident.span(), available_fields,
        ));
    }
}

let (idl, instruction_name) = match instruction_lookup {
    Ok(value) => value,
    Err(error) => {
        errors.push(idl_error_to_syn(...));
        continue;
    }
};

// IDL-dependent checks follow ...
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix: stabilize event lookup handling acr..." | Re-trigger Greptile

Comment on lines 65 to 83
@@ -69,7 +72,7 @@ pub fn parse_url_template(s: &str) -> Vec<UrlTemplatePart> {
}
let close = rest[open..]
.find('}')
.expect("Unclosed '{' in URL template")
.ok_or_else(|| syn::Error::new(span, format!("Unclosed '{{' in URL template: {s}")))?
+ open;
let field_ref = rest[open + 1..close].trim().to_string();
parts.push(UrlTemplatePart::FieldRef(field_ref));
@@ -80,7 +83,7 @@ pub fn parse_url_template(s: &str) -> Vec<UrlTemplatePart> {
parts.push(UrlTemplatePart::Literal(rest.to_string()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty field reference in URL template not validated at compile time

parse_url_template now correctly rejects unclosed braces ({ without }). However, it does not validate that the captured field_ref is non-empty after trimming. A template such as "https://api.example.com/{}/data" or "https://api.example.com/{ }/data" will silently produce a UrlTemplatePart::FieldRef(""), which causes a runtime panic/error when the resolver attempts to look up an empty field path — with no compile-time warning.

Since the PR's intent is to surface these errors eagerly during macro expansion, this case is a natural extension of the same validation:

let field_ref = rest[open + 1..close].trim().to_string();
if field_ref.is_empty() {
    return Err(syn::Error::new(
        span,
        format!("Empty field reference '{{}}' in URL template: {s}"),
    ));
}
parts.push(UrlTemplatePart::FieldRef(field_ref));
Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-macros/src/stream_spec/entity.rs
Line: 65-83

Comment:
**Empty field reference in URL template not validated at compile time**

`parse_url_template` now correctly rejects unclosed braces (`{` without `}`). However, it does not validate that the captured `field_ref` is non-empty after trimming. A template such as `"https://api.example.com/{}/data"` or `"https://api.example.com/{   }/data"` will silently produce a `UrlTemplatePart::FieldRef("")`, which causes a runtime panic/error when the resolver attempts to look up an empty field path — with no compile-time warning.

Since the PR's intent is to surface these errors eagerly during macro expansion, this case is a natural extension of the same validation:

```rust
let field_ref = rest[open + 1..close].trim().to_string();
if field_ref.is_empty() {
    return Err(syn::Error::new(
        span,
        format!("Empty field reference '{{}}' in URL template: {s}"),
    ));
}
parts.push(UrlTemplatePart::FieldRef(field_ref));
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +660 to +713
fn validate_event_references(
entity_name: &str,
events_by_instruction: &HashMap<String, Vec<(String, parse::EventAttribute, syn::Type)>>,
known_fields: &HashSet<String>,
available_fields: &[String],
idls: IdlLookup,
errors: &mut ErrorCollector,
) {
for (instruction_key, event_mappings) in events_by_instruction {
for (_target_field, event_attr, _field_type) in event_mappings {
let instruction_lookup = resolve_instruction_lookup(event_attr, instruction_key, idls);

let (idl, instruction_name) = match instruction_lookup {
Ok(value) => value,
Err(error) => {
errors.push(idl_error_to_syn(
event_attr.instruction_span.unwrap_or(event_attr.attr_span),
error,
));
continue;
}
};

for field_spec in &event_attr.capture_fields {
if let Err(error) =
validate_instruction_field_spec(idl, &instruction_name, field_spec)
{
errors.push(idl_error_to_syn(field_spec.ident.span(), error));
}
}

if let Some(field_spec) = &event_attr.lookup_by {
if let Err(error) =
validate_instruction_field_spec(idl, &instruction_name, field_spec)
{
errors.push(idl_error_to_syn(field_spec.ident.span(), error));
}
}

if let Some(join_on) = &event_attr.join_on {
let reference = join_on.ident.to_string();
if !known_fields.contains(&reference) {
errors.push(entity_field_error(
entity_name,
&reference,
"join_on field",
join_on.ident.span(),
available_fields,
));
}
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 join_on entity-field validation is gated behind a successful IDL lookup

validate_event_references validates event_attr.join_on as a reference to a known entity field (lines 699–710). This check is only reached if the IDL lookup for the instruction succeeds — when it fails, the function continues before reaching the join_on block.

join_on on an #[event] attribute names an entity field, not an instruction argument, so its validity is entirely independent of whether the IDL is available. A stale IDL or an unrecognised instruction path would suppress the entity-field check, masking a misspelled join_on target:

// validate join_on against entity fields regardless of IDL availability
if let Some(join_on) = &event_attr.join_on {
    let reference = join_on.ident.to_string();
    if !known_fields.contains(&reference) {
        errors.push(entity_field_error(
            entity_name, &reference, "join_on field",
            join_on.ident.span(), available_fields,
        ));
    }
}

let (idl, instruction_name) = match instruction_lookup {
    Ok(value) => value,
    Err(error) => {
        errors.push(idl_error_to_syn(...));
        continue;
    }
};

// IDL-dependent checks follow ...
Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-macros/src/validation/mod.rs
Line: 660-713

Comment:
**`join_on` entity-field validation is gated behind a successful IDL lookup**

`validate_event_references` validates `event_attr.join_on` as a reference to a known entity field (lines 699–710). This check is only reached if the IDL lookup for the instruction succeeds — when it fails, the function `continue`s before reaching the `join_on` block.

`join_on` on an `#[event]` attribute names an **entity** field, not an instruction argument, so its validity is entirely independent of whether the IDL is available. A stale IDL or an unrecognised instruction path would suppress the entity-field check, masking a misspelled `join_on` target:

```rust
// validate join_on against entity fields regardless of IDL availability
if let Some(join_on) = &event_attr.join_on {
    let reference = join_on.ident.to_string();
    if !known_fields.contains(&reference) {
        errors.push(entity_field_error(
            entity_name, &reference, "join_on field",
            join_on.ident.span(), available_fields,
        ));
    }
}

let (idl, instruction_name) = match instruction_lookup {
    Ok(value) => value,
    Err(error) => {
        errors.push(idl_error_to_syn(...));
        continue;
    }
};

// IDL-dependent checks follow ...
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant