Add optional regex constraints

This commit is contained in:
Keisuke Hirata 2026-06-16 11:27:27 +09:00
parent 84680e2652
commit f1a5836247
No known key found for this signature in database
9 changed files with 160 additions and 6 deletions

47
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "decodal"
version = "0.1.0"
@ -12,3 +21,41 @@ dependencies = [
[[package]]
name = "decodal-core"
version = "0.1.0"
dependencies = [
"regex",
]
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"

View File

@ -3,5 +3,9 @@ name = "decodal"
version = "0.1.0"
edition = "2024"
[features]
default = []
regex = ["decodal-core/regex"]
[dependencies]
decodal-core = { path = "../decodal-core" }

View File

@ -6,6 +6,7 @@ edition = "2024"
[features]
default = ["std"]
std = []
regex = []
regex = ["std", "dep:regex"]
[dependencies]
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }

View File

@ -707,11 +707,7 @@ impl<L: SourceLoader> Engine<L> {
"value does not satisfy comparison constraint",
)
}),
Constraint::Regex(_) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
"regex constraints require a future regex feature",
)),
Constraint::Regex(pattern) => satisfies_regex(value, pattern, span),
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
@ -967,6 +963,42 @@ fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bo
)
}
#[cfg(feature = "regex")]
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
return Err(Diagnostic::new(
DiagnosticKind::ConstraintViolation,
span,
"regex constraints require a string value",
));
};
let regex = regex::Regex::new(pattern).map_err(|error| {
Diagnostic::new(
DiagnosticKind::Syntax,
span,
format!("invalid regex constraint `{pattern}`: {error}"),
)
})?;
if regex.is_match(value) {
Ok(())
} else {
Err(Diagnostic::new(
DiagnosticKind::ConstraintViolation,
span,
"value does not satisfy regex constraint",
))
}
}
#[cfg(not(feature = "regex"))]
fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<()> {
Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
"regex constraints require the regex feature",
))
}
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
match (value, expected) {
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
@ -1169,4 +1201,22 @@ mod tests {
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[1].value, Data::String(String::from("local")));
}
#[cfg(feature = "regex")]
#[test]
fn regex_constraint_matches_concrete_string() {
let data = eval_data(r#"value = String & /^api-[0-9]+$/ default "api-123";"#);
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::String(String::from("api-123")));
}
#[cfg(feature = "regex")]
#[test]
fn regex_constraint_rejects_non_matching_string() {
let parsed =
crate::parse_source(r#"value = String & /^api-[0-9]+$/ default "web-123";"#).unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let value = engine.eval_root().unwrap();
assert!(engine.materialize(&value).is_err());
}
}

View File

@ -0,0 +1,34 @@
# Features
Decodal keeps the embedded core small by making heavy functionality opt-in.
## Core defaults
`decodal-core` defaults to `std` only.
```toml
[features]
default = ["std"]
std = []
regex = ["std", "dep:regex"]
```
Building `decodal-core` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
## Regex
Regex constraints are implemented behind the `regex` feature.
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
```sh
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
Regex constraints are accumulated during `&` composition.
The implementation does not try to prove whether the intersection of two regex constraints is empty.
Concrete strings must match every regex constraint attached to the abstract value.
## CLI features
`decodal-cli` exposes a matching `regex` feature that enables `decodal-core/regex`.
The feature is not enabled by default so the default CLI binary remains small.

View File

@ -16,3 +16,4 @@ bytecode VM や JIT ではなく、AST を demand-driven に評価すること
4. [Composition and Materialization](./composition-and-materialization.md)
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
6. [Embedding API](./embedding-api.md)
7. [Features](./features.md)

View File

@ -42,4 +42,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
4. [Composition and Materialization](./design/composition-and-materialization.md)
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
6. [Embedding API](./design/embedding-api.md)
7. [Features](./design/features.md)
4. [Open Issues](./open-issues.md)

9
examples/regex/README.md Normal file
View File

@ -0,0 +1,9 @@
# Regex example
Regex constraints are optional. Run this example with the `regex` feature enabled:
```sh
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
Without the feature, regex validation returns an unsupported feature diagnostic.

7
examples/regex/main.dcdl Normal file
View File

@ -0,0 +1,7 @@
let
Identifier = String & /^[A-Za-z_][A-Za-z0-9_]*$/;
ApiName = Identifier & /^api_/;
in
{
service = ApiName default "api_gateway";
}