Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4b10944
Implement proper float32/float64 precision and range conversions
Flaque Jan 8, 2026
0ead132
Implement validation errors for undefined fields and type mismatches
Flaque Jan 8, 2026
8215496
Add range validation for enum conversions
Flaque Jan 8, 2026
2c43d55
Fix timestamp method timezone handling in CEL
Flaque Jan 8, 2026
de79882
Add support for protobuf extension fields
Flaque Jan 8, 2026
cb3ef27
Implement error propagation short-circuit in comprehension evaluation
Flaque Jan 8, 2026
2802507
Merge pull request #1 from sfcompute/magnet/implement-proper-float32f…
Flaque Jan 8, 2026
78dded2
Merge pull request #5 from sfcompute/magnet/implement-error-propagati…
Flaque Jan 8, 2026
2757867
Merge pull request #2 from sfcompute/magnet/implement-validation-erro…
Flaque Jan 8, 2026
d6dfc53
Merge pull request #3 from sfcompute/magnet/add-range-validation-for-…
Flaque Jan 8, 2026
c3edc6f
Merge pull request #4 from sfcompute/magnet/fix-timestamp-method-time…
Flaque Jan 8, 2026
2019324
Merge pull request #6 from sfcompute/magnet/add-support-for-protobuf-…
Flaque Jan 8, 2026
b5099f7
Add conformance test harness from conformance branch
Flaque Jan 8, 2026
b95f8eb
Merge pull request #9 from sfcompute/magnet/conformance-tests-in-sepa…
Flaque Jan 8, 2026
e1ae844
Add conformance tests for type checking and has() in macros
Flaque Jan 8, 2026
e825d3f
Fix cargo test failure by removing non-existent proto feature
Flaque Jan 8, 2026
ce0f72d
Fix compilation errors from master branch
Flaque Jan 8, 2026
dc19846
Merge pull request #8 from sfcompute/magnet/fix-type-checking-and-has…
Flaque Jan 8, 2026
1b5f759
Merge pull request #10 from sfcompute/magnet/cargo-test-failure-confl…
Flaque Jan 8, 2026
230e822
Fix build broken: Add missing Struct type and proto_compare module
Flaque Jan 8, 2026
1eb1a10
Merge pull request #11 from sfcompute/magnet/build-broken
Flaque Jan 8, 2026
b7517f2
Implement qualified identifier resolution for namespace-aware type lo…
Flaque Jan 8, 2026
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "cel-spec"]
path = cel-spec
url = https://github.com/google/cel-spec.git
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["cel", "example", "fuzz"]
members = ["cel", "example", "fuzz", "conformance"]
resolver = "2"

[profile.bench]
Expand Down
1 change: 1 addition & 0 deletions cel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ default = ["regex", "chrono"]
json = ["dep:serde_json", "dep:base64"]
regex = ["dep:regex"]
chrono = ["dep:chrono"]
proto = [] # Proto feature for conformance tests
dhat-heap = [ ] # if you are doing heap profiling
4 changes: 4 additions & 0 deletions cel/src/common/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub struct SelectExpr {
pub operand: Box<IdedExpr>,
pub field: String,
pub test: bool,
/// is_extension indicates whether the field access uses protobuf extension syntax.
/// Extension fields are accessed using msg.(ext.field) syntax where the parentheses
/// indicate an extension field lookup.
pub is_extension: bool,
}

#[derive(Clone, Debug, Default, PartialEq)]
Expand Down
151 changes: 150 additions & 1 deletion cel/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::extensions::ExtensionRegistry;
use crate::magic::{Function, FunctionRegistry, IntoFunction};
use crate::objects::{TryIntoValue, Value};
use crate::parser::Expression;
Expand Down Expand Up @@ -35,11 +36,14 @@ pub enum Context<'a> {
functions: FunctionRegistry,
variables: BTreeMap<String, Value>,
resolver: Option<&'a dyn VariableResolver>,
extensions: ExtensionRegistry,
container: Option<String>,
},
Child {
parent: &'a Context<'a>,
variables: BTreeMap<String, Value>,
resolver: Option<&'a dyn VariableResolver>,
container: Option<String>,
},
}

Expand Down Expand Up @@ -100,6 +104,7 @@ impl<'a> Context<'a> {
variables,
parent,
resolver,
container,
} => resolver
.and_then(|r| r.resolve(name))
.or_else(|| {
Expand All @@ -108,23 +113,140 @@ impl<'a> Context<'a> {
.cloned()
.or_else(|| parent.get_variable(name).ok())
})
.or_else(|| {
// Try qualified name resolution with container
if let Some(container_name) = container {
self.try_qualified_lookup(name, container_name, variables, Some(parent))
} else {
None
}
})
.ok_or_else(|| ExecutionError::UndeclaredReference(name.to_string().into())),
Context::Root {
variables,
resolver,
container,
..
} => resolver
.and_then(|r| r.resolve(name))
.or_else(|| variables.get(name).cloned())
.or_else(|| {
// Try qualified name resolution with container
if let Some(container_name) = container {
self.try_qualified_lookup(name, container_name, variables, None)
} else {
None
}
})
.ok_or_else(|| ExecutionError::UndeclaredReference(name.to_string().into())),
}
}

pub(crate) fn get_function(&self, name: &str) -> Option<&Function> {
/// Attempts to resolve a variable name using qualified name resolution.
///
/// According to the CEL spec, when a container is set, identifiers should be resolved
/// by trying progressively shorter prefixes. For example, if the container is "a.b.c"
/// and we're looking for identifier "x", we should try:
/// 1. "x" (already tried in get_variable)
/// 2. "a.b.c.x"
/// 3. "a.b.x"
/// 4. "a.x"
fn try_qualified_lookup(
&self,
name: &str,
container: &str,
variables: &BTreeMap<String, Value>,
parent: Option<&Context<'_>>,
) -> Option<Value> {
// Build a list of candidate names to try
let mut candidates = Vec::new();

// Add the fully qualified name
candidates.push(format!("{}.{}", container, name));

// Add progressively shorter prefixes
let parts: Vec<&str> = container.split('.').collect();
for i in (1..parts.len()).rev() {
let prefix = parts[..i].join(".");
candidates.push(format!("{}.{}", prefix, name));
}

// Try each candidate
for candidate in candidates {
// Check in current context's variables
if let Some(value) = variables.get(&candidate) {
return Some(value.clone());
}

// Check in parent context if available
if let Some(parent) = parent {
if let Ok(value) = parent.get_variable(&candidate) {
return Some(value);
}
}
}

None
}

pub fn get_extension_registry(&self) -> Option<&ExtensionRegistry> {
match self {
Context::Root { extensions, .. } => Some(extensions),
Context::Child { parent, .. } => parent.get_extension_registry(),
}
}

pub fn get_extension_registry_mut(&mut self) -> Option<&mut ExtensionRegistry> {
match self {
Context::Root { extensions, .. } => Some(extensions),
Context::Child { .. } => None,
}
}

pub(crate) fn get_function(&self, name: &str) -> Option<&Function> {
// First try direct lookup
let direct = match self {
Context::Root { functions, .. } => functions.get(name),
Context::Child { parent, .. } => parent.get_function(name),
};

if direct.is_some() {
return direct;
}

// Try qualified name resolution with container
let container_name = self.get_container()?;

// Build a list of candidate names to try
let mut candidates = Vec::new();

// Add the fully qualified name
candidates.push(format!("{}.{}", container_name, name));

// Add progressively shorter prefixes
let parts: Vec<&str> = container_name.split('.').collect();
for i in (1..parts.len()).rev() {
let prefix = parts[..i].join(".");
candidates.push(format!("{}.{}", prefix, name));
}

// Try each candidate
for candidate in &candidates {
match self {
Context::Root { functions, .. } => {
if let Some(func) = functions.get(candidate) {
return Some(func);
}
}
Context::Child { parent, .. } => {
if let Some(func) = parent.get_function(candidate) {
return Some(func);
}
}
}
}

None
}

pub fn add_function<T: 'static, F>(&mut self, name: &str, value: F)
Expand All @@ -149,6 +271,28 @@ impl<'a> Context<'a> {
parent: self,
variables: Default::default(),
resolver: None,
container: None,
}
}

pub fn with_container(mut self, container: String) -> Self {
match &mut self {
Context::Root { container: c, .. } => {
*c = Some(container);
}
Context::Child { container: c, .. } => {
*c = Some(container);
}
}
self
}

pub fn get_container(&self) -> Option<&str> {
match self {
Context::Root { container, .. } => container.as_deref(),
Context::Child { container, parent, .. } => {
container.as_deref().or_else(|| parent.get_container())
}
}
}

Expand All @@ -168,6 +312,8 @@ impl<'a> Context<'a> {
variables: Default::default(),
functions: Default::default(),
resolver: None,
extensions: ExtensionRegistry::new(),
container: None,
}
}
}
Expand All @@ -178,6 +324,8 @@ impl Default for Context<'_> {
variables: Default::default(),
functions: Default::default(),
resolver: None,
extensions: ExtensionRegistry::new(),
container: None,
};

ctx.add_function("contains", functions::contains);
Expand All @@ -189,6 +337,7 @@ impl Default for Context<'_> {
ctx.add_function("string", functions::string);
ctx.add_function("bytes", functions::bytes);
ctx.add_function("double", functions::double);
ctx.add_function("float", functions::float);
ctx.add_function("int", functions::int);
ctx.add_function("uint", functions::uint);
ctx.add_function("optional.none", functions::optional_none);
Expand Down
Loading