///|
priv struct ConstraintIntRange {
lower : Double
upper : Double
}
///|
priv struct ConstraintIntCall {
name : String
argument : Double
}
///|
// PKL-112: threshold encoding lifted from Int to Double so that
// `Float(isBetween(0.5, 1.5))` parses and runs without losing precision.
// Int-side comparisons widen the value to Double before applying the
// operator (see pkl_constraint_predicate_accepts), so existing Int-only
// constraint annotations keep working unchanged.
priv enum ConstraintIntPredicate {
IsBetween(Double, Double)
IsPositive
IsGreaterThan(Double)
IsLessThan(Double)
NotIsBetween(Double, Double)
NotIsPositive
NotIsGreaterThan(Double)
NotIsLessThan(Double)
CustomGreaterThan(String, Double)
CustomLessThan(String, Double)
CustomGreaterOrEqual(String, Double)
CustomLessOrEqual(String, Double)
NotCustomGreaterThan(String, Double)
NotCustomLessThan(String, Double)
NotCustomGreaterOrEqual(String, Double)
NotCustomLessOrEqual(String, Double)
// PKL-148b: `Int(this > 0)` / `Int(0 < this)`-style bare comparison
// constraints. The constraint name renders as the literal source
// ("this > 0") to match Apple Pkl's diagnostic wording, and the
// accepts side runs the comparison with the candidate substituted
// for `this`.
ThisCompare(ConstraintCompareOp, Double, Bool)
NotThisCompare(ConstraintCompareOp, Double, Bool)
}
///|
priv enum ConstraintCompareOp {
CmpGreaterThan
CmpGreaterOrEqual
CmpLessThan
CmpLessOrEqual
CmpEqual
CmpNotEqual
}
///|
fn pkl_parse_constraint_int_text(text : String) -> Int? {
if text.length() == 0 {
return None
}
let mut index = 0
let mut sign = 1
if text[0].to_int().unsafe_to_char() == '-' {
sign = -1
index = 1
}
if index == text.length() {
return None
}
let mut value = 0
while index < text.length() {
let char = text[index].to_int().unsafe_to_char()
if char < '0' || char > '9' {
return None
}
value = value * 10 + char.to_int() - '0'.to_int()
index += 1
}
Some(value * sign)
}
///|
// PKL-112: parser for Int or Float threshold literals appearing inside
// constraint predicates (`isBetween(0.5, 1.5)`, `isGreaterThan(-3)`).
// Accepts an optional leading `-`, a digit-run integer part, and an
// optional `.` fractional part. Exponents are not handled — the
// parser surface (PKL-128) covers `1e10` separately and constraint text
// never contains scientific literals today.
fn pkl_parse_constraint_double_text(text : String) -> Double? {
let trimmed = pkl_constraint_trim(text)
if trimmed.length() == 0 {
return None
}
let mut index = 0
let mut sign = 1.0
if trimmed[0].to_int().unsafe_to_char() == '-' {
sign = -1.0
index = 1
}
if index == trimmed.length() {
return None
}
let mut int_part = 0.0
let mut saw_digit = false
while index < trimmed.length() {
let char = trimmed[index].to_int().unsafe_to_char()
if char >= '0' && char <= '9' {
int_part = int_part * 10.0 + (char.to_int() - '0'.to_int()).to_double()
saw_digit = true
index += 1
} else {
break
}
}
let mut frac_part = 0.0
let mut frac_scale = 1.0
let mut saw_dot = false
if index < trimmed.length() && trimmed[index].to_int().unsafe_to_char() == '.' {
saw_dot = true
index += 1
while index < trimmed.length() {
let char = trimmed[index].to_int().unsafe_to_char()
if char >= '0' && char <= '9' {
frac_scale = frac_scale * 10.0
frac_part = frac_part +
(char.to_int() - '0'.to_int()).to_double() / frac_scale
saw_digit = true
index += 1
} else {
break
}
}
}
if index != trimmed.length() || !saw_digit {
return None
}
let _ = saw_dot
Some(sign * (int_part + frac_part))
}
///|
fn pkl_split_constraint_arguments(text : String) -> Array[String] {
// Split a comma-separated constraint argument list at the top
// level (parens / brackets / angles balanced). Each segment is
// trimmed so the predicate dispatcher doesn't have to defend
// against leading / trailing whitespace introduced by the
// operator spacing in `Int(isPositive, isLessThan(10))`.
//
// Fast path: a flat argument-free call like `isPositive` has no
// commas; return the trimmed text without entering the per-char
// StringBuilder loop. Also use `write_char` instead of allocating a
// transient one-char String per iteration.
if !string_contains_char(text, ',') {
return [trim_spaces(text)]
}
let parts : Array[String] = []
let buf = StringBuilder::new()
let mut parens = 0
let mut brackets = 0
let mut angles = 0
for char in text {
if char == ',' && parens == 0 && brackets == 0 && angles == 0 {
parts.push(trim_spaces(buf.to_string()))
buf.reset()
} else {
if char == '(' {
parens += 1
} else if char == ')' && parens > 0 {
parens -= 1
} else if char == '[' {
brackets += 1
} else if char == ']' && brackets > 0 {
brackets -= 1
} else if char == '<' {
angles += 1
} else if char == '>' && angles > 0 {
angles -= 1
}
buf.write_char(char)
}
}
let last = trim_spaces(buf.to_string())
if last != "" || parts.length() > 0 {
parts.push(last)
}
parts
}
///|
/// Tight byte-loop single-char index search. Returns `-1` when not
/// found. MoonBit's `String.find(pattern)` routes through
/// `brute_force_find` / `boyer_moore_horspool_find` which dominate
/// the reflect / type-name profile when used with single-char
/// patterns; this avoids that dispatch.
fn string_index_of_char(s : String, c : Char) -> Int {
let target = c.to_int()
for i = 0; i < s.length(); i = i + 1 {
if s[i].to_int() == target {
return i
}
}
-1
}
///|
fn string_contains_char(s : String, c : Char) -> Bool {
string_index_of_char(s, c) >= 0
}
///|
/// Cheap "starts with this one character" check that avoids
/// `String.has_prefix("(")`'s boyer-moore dispatch on the hot reflect
/// / type-name paths.
fn string_starts_with_char(s : String, c : Char) -> Bool {
s.length() > 0 && s[0].to_int() == c.to_int()
}
///|
fn string_ends_with_char(s : String, c : Char) -> Bool {
s.length() > 0 && s[s.length() - 1].to_int() == c.to_int()
}
///|
fn trim_spaces(text : String) -> String {
let mut start = 0
while start < text.length() {
let c = text[start].to_int().unsafe_to_char()
if c == ' ' || c == '\t' {
start = start + 1
} else {
break
}
}
let mut end = text.length()
while end > start {
let c = text[end - 1].to_int().unsafe_to_char()
if c == ' ' || c == '\t' {
end = end - 1
} else {
break
}
}
String::unsafe_substring(text, start~, end~)
}
///|
fn pkl_constrained_type_paren_index(name : String) -> Int? {
// Fast path: a constrained-type name (`Int(isBetween(...))`) must
// contain at least one `(`. Type names without a paren — `Int`,
// `Listing`, `String` — are by far the common case on the
// reflect / synthesize-default hot paths, and they all bail here
// without entering the per-char balanced-bracket walk. Use a tight
// byte-loop instead of `String.find("(")` so the common-case bail
// doesn't itself go through MoonBit's general pattern matcher.
let first_paren = string_index_of_char(name, '(')
match first_paren {
-1 => return None
0 => return None
_ => ()
}
let mut parens = 0
let mut brackets = 0
let mut angles = 0
for i = 0; i < name.length(); i = i + 1 {
let char = name[i].to_int().unsafe_to_char()
if char == '(' && parens == 0 && brackets == 0 && angles == 0 {
return Some(i)
}
if char == '(' {
parens += 1
} else if char == ')' && parens > 0 {
parens -= 1
} else if char == '[' {
brackets += 1
} else if char == ']' && brackets > 0 {
brackets -= 1
} else if char == '<' && parens == 0 && brackets == 0 {
angles += 1
} else if char == '>' && parens == 0 && brackets == 0 && angles > 0 {
angles -= 1
}
}
None
}
///|
fn pkl_constrained_type_suffix_is_balanced(name : String, start : Int) -> Bool {
let mut parens = 0
let mut brackets = 0
let mut angles = 0
for i = start; i < name.length(); i = i + 1 {
let char = name[i].to_int().unsafe_to_char()
if char == '(' {
parens += 1
} else if char == ')' {
parens -= 1
if parens < 0 {
return false
}
} else if char == '[' {
brackets += 1
} else if char == ']' {
brackets -= 1
if brackets < 0 {
return false
}
} else if char == '<' && parens == 0 && brackets == 0 {
angles += 1
} else if char == '>' && parens == 0 && brackets == 0 {
angles -= 1
if angles < 0 {
return false
}
}
}
parens == 0 && brackets == 0 && angles == 0
}
///|
fn pkl_constrained_type_base_name(name : String) -> String? {
match pkl_constrained_type_paren_index(name) {
Some(index) =>
if string_ends_with_char(name, ')') &&
pkl_constrained_type_suffix_is_balanced(name, index) {
Some(String::unsafe_substring(name, start=0, end=index))
} else {
None
}
None => None
}
}
///|
fn pkl_constrained_type_constraint_text(name : String) -> String? {
match pkl_constrained_type_paren_index(name) {
Some(index) =>
if string_ends_with_char(name, ')') &&
pkl_constrained_type_suffix_is_balanced(name, index) {
Some(
String::unsafe_substring(name, start=index + 1, end=name.length() - 1),
)
} else {
None
}
None => None
}
}
///|
fn pkl_builtin_type_alias_target(name : String) -> String? {
match name {
"NonNull" => Some("Any(!(this is Null))")
"UInt" => Some("Int(isPositive)")
"UInt8" => Some("Int(isBetween(0, 255))")
"UInt16" => Some("Int(isBetween(0, 65535))")
"UInt32" => Some("Int(isBetween(0, 4294967295))")
"Int8" => Some("Int(isBetween(-128, 127))")
"Int16" => Some("Int(isBetween(-32768, 32767))")
"Int32" => Some("Int(isBetween(-2147483648, 2147483647))")
"Uri" => Some("String")
_ => None
}
}
///|
fn pkl_constrained_any_not_null_constraint_name(type_name : String) -> String? {
match pkl_constrained_type_base_name(type_name) {
Some("Any") =>
match pkl_constrained_type_constraint_text(type_name) {
Some(text) => {
let parts = pkl_split_constraint_arguments(text)
for part in parts {
if pkl_constraint_trim(part) == "!(this is Null)" {
return Some("!(this is Null)")
}
}
None
}
None => None
}
_ => None
}
}
///|
fn pkl_constraint_call_inner(text : String, prefix : String) -> String? {
if text.has_prefix(prefix) &&
text.has_suffix(")") &&
text.length() >= prefix.length() + 1 {
Some(
String::unsafe_substring(
text,
start=prefix.length(),
end=text.length() - 1,
),
)
} else {
None
}
}
///|
fn pkl_is_between_inner(text : String) -> String? {
match pkl_constraint_call_inner(text, "isBetween(") {
Some(inner) => Some(inner)
None => pkl_constraint_call_inner(text, "this.isBetween(")
}
}
///|
fn pkl_is_between_range(text : String) -> ConstraintIntRange? {
match pkl_is_between_inner(text) {
Some(inner) => {
let parts = pkl_split_constraint_arguments(inner)
if parts.length() != 2 {
return None
}
match
(
pkl_parse_constraint_double_text(parts[0]),
pkl_parse_constraint_double_text(parts[1]),
) {
(Some(lower), Some(upper)) => Some({ lower, upper })
_ => None
}
}
None => None
}
}
///|
fn pkl_constraint_property_matches(text : String, name : String) -> Bool {
text == name || text == "this." + name
}
///|
fn pkl_single_int_constraint_argument(text : String, name : String) -> Double? {
match pkl_constraint_call_inner(text, name + "(") {
Some(inner) => pkl_parse_constraint_double_text(inner)
None =>
match pkl_constraint_call_inner(text, "this." + name + "(") {
Some(inner) => pkl_parse_constraint_double_text(inner)
None => None
}
}
}
///|
fn pkl_single_int_constraint_call(text : String) -> ConstraintIntCall? {
let call_text = if text.has_prefix("this.") && text.length() > 5 {
String::unsafe_substring(text, start=5, end=text.length())
} else {
text
}
match call_text.find("(") {
Some(index) =>
if index == 0 || !call_text.has_suffix(")") {
None
} else {
let name = String::unsafe_substring(call_text, start=0, end=index)
let inner = String::unsafe_substring(
call_text,
start=index + 1,
end=call_text.length() - 1,
)
match pkl_parse_constraint_double_text(inner) {
Some(argument) => Some({ name, argument })
None => None
}
}
None => None
}
}
///|
fn pkl_int_constraint_predicate(text : String) -> ConstraintIntPredicate? {
if text.has_prefix("!") && text.length() > 1 {
let inner = String::unsafe_substring(text, start=1, end=text.length())
match pkl_int_constraint_predicate(inner) {
Some(IsBetween(lower, upper)) => return Some(NotIsBetween(lower, upper))
Some(IsPositive) => return Some(NotIsPositive)
Some(IsGreaterThan(threshold)) => return Some(NotIsGreaterThan(threshold))
Some(IsLessThan(threshold)) => return Some(NotIsLessThan(threshold))
Some(ThisCompare(op, t, leftish)) =>
return Some(NotThisCompare(op, t, leftish))
_ => return None
}
}
match pkl_is_between_range(text) {
Some(range) => return Some(IsBetween(range.lower, range.upper))
None => ()
}
if pkl_constraint_property_matches(text, "isPositive") {
return Some(IsPositive)
}
match pkl_single_int_constraint_argument(text, "isGreaterThan") {
Some(threshold) => return Some(IsGreaterThan(threshold))
None => ()
}
match pkl_single_int_constraint_argument(text, "isLessThan") {
Some(threshold) => return Some(IsLessThan(threshold))
None => ()
}
// PKL-148b: `this N` bare comparison falls through last so it
// doesn't shadow the named-predicate forms above.
match pkl_parse_this_comparison(text) {
Some(predicate) => return Some(predicate)
None => ()
}
None
}
///|
fn pkl_user_defined_int_constraint_from_order(
function_name : String,
threshold : Double,
value_on_left : Bool,
op : BinaryOp,
) -> ConstraintIntPredicate? {
match op {
GreaterThan =>
if value_on_left {
Some(CustomGreaterThan(function_name, threshold))
} else {
Some(CustomLessThan(function_name, threshold))
}
LessThan =>
if value_on_left {
Some(CustomLessThan(function_name, threshold))
} else {
Some(CustomGreaterThan(function_name, threshold))
}
GreaterOrEqual =>
if value_on_left {
Some(CustomGreaterOrEqual(function_name, threshold))
} else {
Some(CustomLessOrEqual(function_name, threshold))
}
LessOrEqual =>
if value_on_left {
Some(CustomLessOrEqual(function_name, threshold))
} else {
Some(CustomGreaterOrEqual(function_name, threshold))
}
_ => None
}
}
///|
fn pkl_user_defined_int_constraint_from_comparison(
function_name : String,
factory_parameter : String,
threshold : Double,
lambda_parameter : String,
op : BinaryOp,
left : Expr,
right : Expr,
) -> ConstraintIntPredicate? {
match (left, right) {
(Identifier(left_name), Identifier(right_name)) =>
if left_name == lambda_parameter && right_name == factory_parameter {
pkl_user_defined_int_constraint_from_order(
function_name, threshold, true, op,
)
} else if left_name == factory_parameter && right_name == lambda_parameter {
pkl_user_defined_int_constraint_from_order(
function_name, threshold, false, op,
)
} else {
None
}
_ => None
}
}
///|
fn pkl_user_defined_int_constraint_from_function_decl(
function_decl : FunctionDecl,
threshold : Double,
) -> ConstraintIntPredicate? {
if function_decl.parameters.length() != 1 {
return None
}
let factory_parameter = function_decl.parameters[0].name
match function_decl.body {
Some(LambdaExpr(lambda_parameters, BinaryExpr(op, left, right), _)) =>
if lambda_parameters.length() == 1 {
pkl_user_defined_int_constraint_from_comparison(
function_decl.name,
factory_parameter,
threshold,
lambda_parameters[0].name,
op,
left,
right,
)
} else {
None
}
_ => None
}
}
///|
fn pkl_negate_user_defined_int_constraint(
predicate : ConstraintIntPredicate,
) -> ConstraintIntPredicate? {
match predicate {
CustomGreaterThan(name, threshold) =>
Some(NotCustomGreaterThan(name, threshold))
CustomLessThan(name, threshold) => Some(NotCustomLessThan(name, threshold))
CustomGreaterOrEqual(name, threshold) =>
Some(NotCustomGreaterOrEqual(name, threshold))
CustomLessOrEqual(name, threshold) =>
Some(NotCustomLessOrEqual(name, threshold))
_ => None
}
}
///|
fn pkl_user_defined_int_constraint_predicate(
text : String,
declarations : Array[Declaration],
) -> ConstraintIntPredicate? {
if text.has_prefix("!") && text.length() > 1 {
let inner = String::unsafe_substring(text, start=1, end=text.length())
match pkl_user_defined_int_constraint_predicate(inner, declarations) {
Some(predicate) =>
return pkl_negate_user_defined_int_constraint(predicate)
None => return None
}
}
match pkl_single_int_constraint_call(text) {
Some(call) => {
for declaration in declarations {
match declaration {
FunctionDeclaration(function_decl) =>
if function_decl.name == call.name {
return pkl_user_defined_int_constraint_from_function_decl(
function_decl,
call.argument,
)
}
ClassDeclaration(_) | TypeAliasDeclaration(_) => ()
}
}
None
}
None => None
}
}
///|
fn pkl_constrained_int_predicates(
type_name : String,
) -> Array[ConstraintIntPredicate] {
let predicates : Array[ConstraintIntPredicate] = []
// PKL-092: accept `Int(...)`, `Float(...)`, and `Number(...)` as the
// numeric constraint host. The predicate-side encoding (Int thresholds)
// is kept — Float values are widened from those thresholds when the
// accepts-float helper runs the comparison.
match pkl_constrained_type_base_name(type_name) {
Some("Int") | Some("Float") | Some("Number") =>
match pkl_constrained_type_constraint_text(type_name) {
Some(text) => {
let parts = pkl_split_constraint_arguments(text)
for part in parts {
match pkl_int_constraint_predicate(part) {
Some(predicate) => predicates.push(predicate)
None => ()
}
}
}
None => ()
}
_ => ()
}
predicates
}
///|
fn pkl_user_defined_constrained_int_predicates(
type_name : String,
declarations : Array[Declaration],
) -> Array[ConstraintIntPredicate] {
let predicates : Array[ConstraintIntPredicate] = []
match pkl_constrained_type_base_name(type_name) {
Some("Int") =>
match pkl_constrained_type_constraint_text(type_name) {
Some(text) => {
let parts = pkl_split_constraint_arguments(text)
for part in parts {
match
pkl_user_defined_int_constraint_predicate(part, declarations) {
Some(predicate) => predicates.push(predicate)
None => ()
}
}
}
None => ()
}
_ => ()
}
predicates
}
///|
fn pkl_lookup_type_alias_target(
declarations : Array[Declaration],
name : String,
) -> String? {
let mut found : String? = None
for declaration in declarations {
match declaration {
TypeAliasDeclaration(type_alias) =>
if type_alias.name == name {
found = Some(type_alias.target)
}
ClassDeclaration(_) | FunctionDeclaration(_) => ()
}
}
found
}
///|
fn pkl_user_defined_constrained_type_source_name_with_depth(
name : String,
declarations : Array[Declaration],
depth : Int,
) -> String? {
if depth > 8 {
return None
}
if pkl_user_defined_constrained_int_predicates(name, declarations).length() >
0 {
return Some(name)
}
match pkl_lookup_type_alias_target(declarations, name) {
Some(target) =>
pkl_user_defined_constrained_type_source_name_with_depth(
target,
declarations,
depth + 1,
)
None => None
}
}
///|
fn pkl_user_defined_constrained_type_source_name(
name : String,
declarations : Array[Declaration],
) -> String? {
pkl_user_defined_constrained_type_source_name_with_depth(
name, declarations, 0,
)
}
///|
fn pkl_constrained_type_annotation_has_supported_constraint(
type_name : String,
) -> Bool {
pkl_constrained_type_annotation_has_supported_constraint_with_depth(
type_name, 0,
)
}
///|
/// Recurse into collection wrappers so `Listing` and
/// `Mapping 0), Int>` register as supported. Depth is
/// capped to keep mutually-recursive aliases from looping the cascade.
fn pkl_constrained_type_annotation_has_supported_constraint_with_depth(
type_name : String,
depth : Int,
) -> Bool {
if depth > 8 {
return false
}
match pkl_builtin_type_alias_target(type_name) {
Some(target) =>
if pkl_constrained_type_annotation_has_supported_constraint_with_depth(
target,
depth + 1,
) {
return true
}
None => ()
}
if pkl_constrained_int_predicates(type_name).length() > 0 ||
pkl_constrained_string_predicates(type_name).length() > 0 ||
pkl_constrained_any_not_null_constraint_name(type_name) is Some(_) {
return true
}
match generic_argument_text(type_name, "Listing") {
Some(inner_text) =>
return pkl_constrained_type_annotation_has_supported_constraint_with_depth(
inner_text,
depth + 1,
)
None => ()
}
match generic_argument_text(type_name, "Mapping") {
Some(inner_text) => {
let parts = split_top_level_generic_arguments(inner_text)
if parts.length() == 2 {
if pkl_constrained_type_annotation_has_supported_constraint_with_depth(
parts[0],
depth + 1,
) ||
pkl_constrained_type_annotation_has_supported_constraint_with_depth(
parts[1],
depth + 1,
) {
return true
}
}
return false
}
None => ()
}
false
}
///|
fn pkl_constraint_name(predicate : ConstraintIntPredicate) -> String {
// PKL-148: include the argument list verbatim so the diagnostic
// wording matches Apple Pkl's exact format (e.g. `isBetween(10, 20)`
// rather than the bare `isBetween`). The threshold values are
// formatted through `format_constraint_arg` so Int-magnitude
// doubles render without a trailing `.0`.
match predicate {
IsBetween(lo, hi) =>
"isBetween(\{format_constraint_arg(lo)}, \{format_constraint_arg(hi)})"
IsPositive => "isPositive"
IsGreaterThan(t) => "isGreaterThan(\{format_constraint_arg(t)})"
IsLessThan(t) => "isLessThan(\{format_constraint_arg(t)})"
NotIsBetween(lo, hi) =>
"!isBetween(\{format_constraint_arg(lo)}, \{format_constraint_arg(hi)})"
NotIsPositive => "!isPositive"
NotIsGreaterThan(t) => "!isGreaterThan(\{format_constraint_arg(t)})"
NotIsLessThan(t) => "!isLessThan(\{format_constraint_arg(t)})"
CustomGreaterThan(name, t) => "\{name}(\{format_constraint_arg(t)})"
CustomLessThan(name, t) => "\{name}(\{format_constraint_arg(t)})"
CustomGreaterOrEqual(name, t) => "\{name}(\{format_constraint_arg(t)})"
CustomLessOrEqual(name, t) => "\{name}(\{format_constraint_arg(t)})"
NotCustomGreaterThan(name, t) =>
"!" + name + "(\{format_constraint_arg(t)})"
NotCustomLessThan(name, t) => "!" + name + "(\{format_constraint_arg(t)})"
NotCustomGreaterOrEqual(name, t) =>
"!" + name + "(\{format_constraint_arg(t)})"
NotCustomLessOrEqual(name, t) =>
"!" + name + "(\{format_constraint_arg(t)})"
ThisCompare(op, t, this_on_left) =>
if this_on_left {
"this \{constraint_compare_op_text(op)} \{format_constraint_arg(t)}"
} else {
"\{format_constraint_arg(t)} \{constraint_compare_op_text(op)} this"
}
NotThisCompare(op, t, this_on_left) =>
if this_on_left {
"!(this \{constraint_compare_op_text(op)} \{format_constraint_arg(t)})"
} else {
"!(\{format_constraint_arg(t)} \{constraint_compare_op_text(op)} this)"
}
}
}
///|
fn constraint_compare_op_text(op : ConstraintCompareOp) -> String {
match op {
CmpGreaterThan => ">"
CmpGreaterOrEqual => ">="
CmpLessThan => "<"
CmpLessOrEqual => "<="
CmpEqual => "=="
CmpNotEqual => "!="
}
}
///|
fn _pkl_binary_op_to_compare(op : BinaryOp) -> ConstraintCompareOp? {
match op {
GreaterThan => Some(CmpGreaterThan)
GreaterOrEqual => Some(CmpGreaterOrEqual)
LessThan => Some(CmpLessThan)
LessOrEqual => Some(CmpLessOrEqual)
Equal => Some(CmpEqual)
NotEqual => Some(CmpNotEqual)
_ => None
}
}
///|
/// PKL-148b: parse a `this N` or `N this` bare comparison
/// from the constraint text into a `ThisCompare` predicate. Whitespace
/// is normalised; the supported ops are >, >=, <, <=, ==, !=.
fn pkl_parse_this_comparison(text : String) -> ConstraintIntPredicate? {
let trimmed = pkl_constraint_trim(text)
let ops : Array[(String, ConstraintCompareOp)] = [
(">=", CmpGreaterOrEqual),
("<=", CmpLessOrEqual),
("==", CmpEqual),
("!=", CmpNotEqual),
(">", CmpGreaterThan),
("<", CmpLessThan),
]
for op_pair in ops {
let op_text = op_pair.0
let op = op_pair.1
match pkl_split_on_op(trimmed, op_text) {
Some((left, right)) => {
let left_trim = pkl_constraint_trim(left)
let right_trim = pkl_constraint_trim(right)
if left_trim == "this" {
match pkl_parse_constraint_double_text(right_trim) {
Some(n) => return Some(ThisCompare(op, n, true))
None => ()
}
} else if right_trim == "this" {
match pkl_parse_constraint_double_text(left_trim) {
Some(n) => return Some(ThisCompare(op, n, false))
None => ()
}
}
}
None => ()
}
}
None
}
///|
fn pkl_split_on_op(text : String, op : String) -> (String, String)? {
let n = text.length()
let m = op.length()
let mut i = 0
while i + m <= n {
let mut equal = true
for j = 0; j < m; j = j + 1 {
if text[i + j] != op[j] {
equal = false
break
}
}
if equal {
// Reject if the op text appears inside a longer operator (e.g.
// matching `>` inside `>=`).
let prev = if i > 0 {
Some(text[i - 1].to_int().unsafe_to_char())
} else {
None
}
let next = if i + m < n {
Some(text[i + m].to_int().unsafe_to_char())
} else {
None
}
let ambiguous = match (op, prev, next) {
(">", _, Some('=')) | ("<", _, Some('=')) => true
("=", Some('='), _)
| ("=", Some('!'), _)
| ("=", Some('<'), _)
| ("=", Some('>'), _) => true
_ => false
}
if !ambiguous {
return Some(
(
String::unsafe_substring(text, start=0, end=i),
String::unsafe_substring(text, start=i + m, end=n),
),
)
}
}
i = i + 1
}
None
}
///|
/// PKL-148: format a constraint threshold for the diagnostic. Int-like
/// magnitudes (e.g. `10.0`) drop their trailing `.0` so the message
/// reads `isBetween(10, 20)` rather than `isBetween(10.0, 20.0)`.
fn format_constraint_arg(value : Double) -> String {
if value.is_nan() || value.is_inf() {
"\{value}"
} else if value == value.floor() && value.abs() < 9.0e15 {
"\{value.to_int64()}"
} else {
"\{value}"
}
}
///|
fn pkl_constraint_predicate_accepts(
predicate : ConstraintIntPredicate,
value : Int64,
) -> Bool {
// PKL-112: thresholds are stored as Double. Widen the Int side to
// Double for the comparison so Int(isBetween(0, 10)) keeps working
// alongside Float(isBetween(0.5, 1.5)).
pkl_constraint_predicate_accepts_float(predicate, value.to_double())
}
///|
fn pkl_constrained_int_rejection_message_from_source(
_display_name : String,
source_name : String,
value : Int64,
) -> String? {
for predicate in pkl_constrained_int_predicates(source_name) {
if !pkl_constraint_predicate_accepts(predicate, value) {
return Some(
// PKL-148: align with Apple Pkl's exact diagnostic wording so
// snippetTest fixtures that capture this string via
// `test.catch(...)` match byte-for-byte.
"Type constraint `\{pkl_constraint_name(predicate)}` violated. Value: \{value}",
)
}
}
None
}
///|
fn pkl_user_defined_constrained_int_rejection_message_from_source(
_display_name : String,
source_name : String,
value : Int64,
declarations : Array[Declaration],
) -> String? {
for
predicate in pkl_user_defined_constrained_int_predicates(
source_name, declarations,
) {
if !pkl_constraint_predicate_accepts(predicate, value) {
return Some(
// PKL-148: align with Apple Pkl's exact diagnostic wording so
// snippetTest fixtures that capture this string via
// `test.catch(...)` match byte-for-byte.
"Type constraint `\{pkl_constraint_name(predicate)}` violated. Value: \{value}",
)
}
}
None
}
///|
fn pkl_int_literal_value(expr : Expr) -> Int64? {
match expr {
IntLiteral(value) => Some(value)
UnaryExpr(Negate, IntLiteral(value)) => Some(0L - value)
_ => None
}
}
///|
fn pkl_constrained_type_annotation_expr_rejection_message_from_source(
display_name : String,
source_name : String,
expr : Expr,
) -> String? {
match pkl_builtin_type_alias_target(source_name) {
Some(target) =>
return pkl_constrained_type_annotation_expr_rejection_message_from_source(
display_name, target, expr,
)
None => ()
}
match expr {
NullLiteral =>
match pkl_constrained_any_not_null_constraint_name(source_name) {
Some(name) =>
return Some("Type constraint `\{name}` violated. Value: null")
None => ()
}
_ => ()
}
match pkl_int_literal_value(expr) {
Some(value) =>
return pkl_constrained_int_rejection_message_from_source(
display_name, source_name, value,
)
None => ()
}
match pkl_string_literal_value(expr) {
Some(value) =>
pkl_constrained_string_rejection_message_from_source(
display_name, source_name, value,
)
None => None
}
}
///|
fn pkl_string_literal_value(expr : Expr) -> String? {
match expr {
StringLiteral(value) => Some(value)
_ => None
}
}
///|
fn pkl_user_defined_constrained_type_annotation_expr_rejection_message(
type_name : String?,
expr : Expr,
declarations : Array[Declaration],
) -> String? {
match type_name {
Some(display_name) =>
match
pkl_user_defined_constrained_type_source_name(
display_name, declarations,
) {
Some(source_name) =>
match pkl_int_literal_value(expr) {
Some(value) =>
pkl_user_defined_constrained_int_rejection_message_from_source(
display_name, source_name, value, declarations,
)
None => None
}
None => None
}
None => None
}
}
///|
fn pkl_user_defined_constrained_type_annotation_expr_rejection_message_from_source(
display_name : String,
source_name : String,
expr : Expr,
declarations : Array[Declaration],
) -> String? {
match
pkl_user_defined_constrained_type_source_name(source_name, declarations) {
Some(resolved_source_name) =>
match pkl_int_literal_value(expr) {
Some(value) =>
pkl_user_defined_constrained_int_rejection_message_from_source(
display_name, resolved_source_name, value, declarations,
)
None => None
}
None => None
}
}