Add primitive body typecheck helpers#881
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
f5db02d to
22481a2
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #881 +/- ##
==========================================
- Coverage 87.17% 86.66% -0.52%
==========================================
Files 88 88
Lines 26106 26439 +333
==========================================
+ Hits 22759 22913 +154
- Misses 3347 3526 +179 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
22481a2 to
2fbd1d1
Compare
f8883c7 to
9024d33
Compare
9024d33 to
28d270c
Compare
|
@coderabbitai full review |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/exec_state.rs`:
- Around line 286-289: The Var arm in ResolvedExpr (ResolvedExpr::Var, using
resolved_var and bindings) only looks up caller-supplied local bindings and thus
treats globals as unbound; update this branch to explicitly handle
resolved_var.is_global_ref: if is_global_ref is true, look up the name in the
runtime/global store (the global bindings source used elsewhere in exec_state)
and return that value, otherwise fall back to the existing local bindings lookup
and preserve the EvalError::UnboundVariable behavior; alternatively, if globals
should be rejected earlier, add a guard where
typecheck_expr_with_bindings_and_output produces ResolvedVar with is_global_ref
and return a clear error before exposing the resolved expression API.
- Around line 114-129: The current apply_table_function gates authorization by
the runtime context via self.ctx(), which allows less-capable wrappers running
under a wider stamped Context to escalate; instead, check the wrapper/state
capability itself before calling action.lookup_or_insert or action.lookup.
Update apply_table_function to require the wrapper's explicit read/write
capability (e.g., a method like allows_write()/allows_read() or a
wrapper_capabilities() bitset) when matching FunctionSubtype::Constructor and
FunctionSubtype::Custom, and use es_mut()/es() only after that capability check;
apply the same change pattern to the other similar block mentioned (lines
231-252) so authorization is based on wrapper capability, not self.ctx().
In `@src/lib.rs`:
- Around line 2241-2250: The code currently calls
resolved_function_for_unstable_fn(...).unwrap_or_else(|err| panic!("{err}")),
which converts a modeled Result failure into a hard panic; instead change
BackendRule::prim (and callers in actions) to return Result and propagate the
error: remove the unwrap_or_else panic branch, propagate the Err(err) from
resolved_function_for_unstable_fn back to the caller of prim (and update call
sites in actions to handle/propagate that Result), and ensure any
panic_id/new_panic usage is only created when you will continue successfully;
i.e., construct panic_id only after resolved_function_for_unstable_fn succeeds
or discard it when returning Err so compilation yields a normal Error rather
than aborting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1a3cac49-7839-4b2c-8349-0a2c63acfe89
📒 Files selected for processing (4)
CHANGELOG.mdsrc/exec_state.rssrc/lib.rssrc/typechecking.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: benchmark (ubuntu-latest, conv1d_128)
- GitHub Check: benchmark (ubuntu-latest, eggcc-2mm)
- GitHub Check: benchmark (ubuntu-latest, rectangle)
- GitHub Check: benchmark (ubuntu-latest, taylor51)
- GitHub Check: benchmark (ubuntu-latest, math-microbenchmark)
- GitHub Check: coverage
- GitHub Check: test
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.md
[style] ~10-~10: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ted separately from database updates. - Add `EGraph::typecheck_expr_with_bindings_a...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🔇 Additional comments (1)
src/lib.rs (1)
2535-2662: LGTM!
yihozhang
left a comment
There was a problem hiding this comment.
Some small questions. Overall looks good
…ve-body-runtime-apis # Conflicts: # CHANGELOG.md # src/lib.rs
| /// `unstable-app` will call later. For primitive targets, this bakes one | ||
| /// dispatch id per runtime context so application can choose the entrypoint | ||
| /// matching the primitive body's current call-site context. | ||
| fn resolve_function_container_target_with_context( |
There was a problem hiding this comment.
Why did this function get refactored? What changed?
There was a problem hiding this comment.
I believe from the merge from main, with the context stuff and also with supporting primtives doing the more advanced typea analysis. LMK if you have issues with how it is now.
yihozhang
left a comment
There was a problem hiding this comment.
Had a small question. Otherwise looks good. Thanks!
|
Hmm, can we chat about this PR before we merge it? I'd like some context |
| /// cannot evaluate the string literal `"f"` directly. This helper replaces | ||
| /// the `(unstable-fn "f")` sub-expression with a fresh hidden variable and | ||
| /// returns a binding from that variable to the prepared function value. | ||
| pub fn prepare_unstable_fn_targets_for_eval( |
There was a problem hiding this comment.
Could you explain why we need this? I'm wary of new unstable-fn related code in core
There was a problem hiding this comment.
This is needed to to do the same translation as in our pass that tests if fn_name == "unstable-fn" or whatever, that replaces the first arg of the function name as a string with the real resolved function value. So it just exposes the current behavior we have in core so others can you use it when preparing a resolved expression. Like this is how its used in the primitive command in experimental:
match egraph.typecheck_expr_with_bindings_and_output(
&args[3],
&bindings,
output_sort.clone(),
context,
) {
Ok(resolved)
if matches!(
(required_context(egraph, &resolved), context),
(Context::Pure, _)
| (Context::Read, Context::Read | Context::Full)
| (Context::Write, Context::Write | Context::Full)
| (Context::Full, Context::Full)
) =>
{
typechecked_body = Some((resolved, context));
break;
}
Ok(_) => {}
Err(err) => last_error = Some(err),
}
}
let Some((body, context)) = typechecked_body else {
return Err(last_error
.expect("primitive body typechecking always tries at least one context")
.into());
};
let (body, hidden_bindings) = egraph.prepare_unstable_fn_targets_for_eval(&body)?;
let primitive = DefinedPrimitive {
name,
input_vars: input_var_names,
input: input_sorts,
output: output_sort,
body,
hidden_bindings,
};I think to make this DRY and do the same code path on both would require more refactoring and make unstable function more in core.
I did have a go at this, but threw out that branch when it added more code, also after talking to @yihozhang about it. The "best" place to put this for overall design is in ResolvedExpr to add a specific case for a Function, but yeah this adds it even more inside core, so decided against that for now...
| /// into the same zero-argument function calls used during command execution. | ||
| /// `context` should match the runtime context where the resolved expression | ||
| /// will be evaluated. | ||
| pub fn typecheck_expr_with_bindings_and_output( |
There was a problem hiding this comment.
Doesn't this mean if users get the binding types wrong they can get ill-typed programs? Is that a concern?
I imagine this is for custom commands
There was a problem hiding this comment.
If they put in the binding types wrong, it won't typecheck I believe? Is that what you mean?
| } | ||
| } | ||
|
|
||
| pub(crate) fn typecheck_expr_with_output( |
There was a problem hiding this comment.
Did we really not have this? Is this duplicate code with existing typecheck expr?
There was a problem hiding this comment.
It's mostly not. We can extract some common code for setting up type checking with an optional output type, but this isn't much code anyway.
There was a problem hiding this comment.
Yeah I did try to see if we could extract out some commonality, but I'll have another go at it, to see if it decreases repetition or just adds more issues.
There was a problem hiding this comment.
I was able to refactor it so there are a few shared helpers here, do you guys like this more? A little more LoC but less duplication:
diff --git a/src/typechecking.rs b/src/typechecking.rs
index dedb17f6..c3623d0f 100644
--- a/src/typechecking.rs
+++ b/src/typechecking.rs
@@ -6,7 +6,8 @@ use crate::{
*,
};
use ast::{
- MappedExprExt, ResolvedAction, ResolvedExpr, ResolvedFact, ResolvedRule, ResolvedVar, Rule,
+ MappedActions, MappedExprExt, ResolvedAction, ResolvedExpr, ResolvedFact, ResolvedRule,
+ ResolvedVar, Rule,
};
use core_relations::ExternalFunction;
use egglog_ast::generic_ast::GenericAction;
@@ -960,6 +961,18 @@ impl TypeInfo {
binding: &IndexMap<&str, (Span, ArcSort)>,
context: Context,
) -> Result<ResolvedActions, TypeError> {
+ let (problem, mapped_action) =
+ self.build_standalone_action_problem(symbol_gen, actions, binding, context)?;
+ self.solve_standalone_action_problem(problem, mapped_action, context)
+ }
+
+ fn build_standalone_action_problem(
+ &self,
+ symbol_gen: &mut SymbolGen,
+ actions: &Actions,
+ binding: &IndexMap<&str, (Span, ArcSort)>,
+ context: Context,
+ ) -> Result<(Problem<AtomTerm, ArcSort>, MappedActions<String, String>), TypeError> {
let mut binding_set: IndexSet<String> =
binding.keys().copied().map(str::to_string).collect();
// We lower to core actions with `union_to_set_optimization`
@@ -975,6 +988,15 @@ impl TypeInfo {
problem.assign_local_var_type(var, span.clone(), sort.clone())?;
}
+ Ok((problem, mapped_action))
+ }
+
+ fn solve_standalone_action_problem(
+ &self,
+ problem: Problem<AtomTerm, ArcSort>,
+ mapped_action: MappedActions<String, String>,
+ context: Context,
+ ) -> Result<ResolvedActions, TypeError> {
let assignment = problem
.solve(|sort: &ArcSort| sort.name())
.map_err(|e| e.to_type_error())?;
@@ -1008,29 +1030,20 @@ impl TypeInfo {
context: Context,
) -> Result<ResolvedExpr, TypeError> {
let action = Action::Expr(expr.span(), expr.clone());
- let mut binding_set: IndexSet<String> =
- binding.keys().copied().map(str::to_string).collect();
- let mut ctx = CoreActionContext::new(self, &mut binding_set, symbol_gen, false);
- let (actions, mapped_action) = Actions::singleton(action).to_core_actions(&mut ctx)?;
- let mut problem = Problem::default();
-
- problem.add_actions(&actions, self, symbol_gen, context)?;
-
- for (var, (span, sort)) in binding {
- problem.assign_local_var_type(var, span.clone(), sort.clone())?;
- }
-
+ let (mut problem, mapped_action) = self.build_standalone_action_problem(
+ symbol_gen,
+ &Actions::singleton(action),
+ binding,
+ context,
+ )?;
let [GenericAction::Expr(_, mapped_expr)] = mapped_action.0.as_slice() else {
unreachable!("typechecking an expression should produce one expression action")
};
let output_atom = mapped_expr.get_corresponding_var_or_lit(self);
problem.add_binding(output_atom, output_sort.clone());
- let assignment = problem
- .solve(|sort: &ArcSort| sort.name())
- .map_err(|e| e.to_type_error())?;
-
- let annotated_actions = assignment.annotate_actions(&mapped_action, self, context)?;
+ let annotated_actions =
+ self.solve_standalone_action_problem(problem, mapped_action, context)?;
match annotated_actions.0.into_iter().next().unwrap() {
ResolvedAction::Expr(_, resolved_expr) => {
let actual = resolved_expr.output_type();|
Do we really need new primitive declarations in the body of a rule? Is that the main motivation for this PR? |
|
@oflatt Thanks for the comments! This is for egraphs-good/egglog-experimental#46 which I needed for the A/C stuff with maps, so you can do:
No we dont have new primitive declarations in the body of a rule, only at the top level! We just have basically like "eval a resolved expression" in the body of a rule, since this |
|
I talked to Oliver and he seems to generally feel OK about most of this PR since its pretty general, except for I tried to see if we could remove that API and instead just do it in experimental, however, we would have to expose the existing We could make a wrapper around that to expose, but I am not sure if that resolves the objection. Details from the agent:
Instead to do this "right" would require refactoring the compiler toolchain to handle unstable function more consistently and as first class. This would be a larger change and would be happy to look into this, but not sure if its right for this PR. |
|
Thanks for exploring. I'm going to approve to unblock this, and we can discuss next steps in the next egglog meeting. |
Context
This is the egglog-side support needed by egraphs-good/egglog-experimental#46 for body-defined
(primitive ...)commands. It intentionally contains reusable core APIs only; the experimental command implementation stays in egglog-experimental.Current shape
EGraph::typecheck_expr_with_bindings_and_outputfor expression typechecking with explicit local bindings, an expected output sort, and a runtime primitive context.Core::eval_resolved_exprso primitive bodies can evaluate already-typechecked and prepared expressions through the current execution state.Core::apply_primitivefor evaluating an already-specialized primitive against runtime values.PrimitiveWithId::is_valid_in_contextso downstream code can infer the minimum runtime context for a resolved primitive target without reaching into the internal context-id map.EGraph::prepare_unstable_fn_targets_for_eval, a narrow bridge that rewrites literal(unstable-fn "...")targets into hidden bindings containing the sameResolvedFunctionvalue that backend rule lowering injects.EGraph::eval_resolved_expron the existing backend-rule execution path.Tried and decided against
PreparedResolvedExpr/ResolvedCoreActionsbridge for direct runtime expression evaluation. Dropped it because it introduced a broader public concept and action-interpreter surface just to support one hiddenunstable-fnruntime value.EGraph::eval_resolved_exprto directExecutionStateevaluation. Kept the backend-rule path instead, since top-level eval already gets the full lowering behavior there and does not need to share the primitive-body direct-eval path.(primitive ...)command implementation and does not include the unrelated core-relations ordering fix from PR Fix multi-column column index rebuild ordering #899.Validation
upstream/main:cargo test --lib typecheck_expr_with --quietcargo test --test typed_primitive --quietcargo check --libgit diff --checkmake nitscargo check --locked --libcargo test --lib --quietcargo test --test naive_unstable_fn_via_global --quietcargo test --test files --features bin -- unstable_fn --quietcargo test --test files --features bin -- typed_primitive_unstable_app --quiet