///|
fn is_string_property_name(name : String) -> Bool {
match name {
"length"
| "isEmpty"
// PKL-148: pkl:base.String surface — `isNotEmpty`, `isBlank`,
// `isNotBlank`, `reverse`, `base64`, plus the surrogate-pair
// accessors.
| "isNotEmpty"
| "isBlank"
| "isNotBlank"
| "reverse"
| "base64"
| "md5"
| "sha1"
| "sha256"
| "sha256Int"
| "codePointCount"
| "codePoints"
| "chars"
// PKL-149: extra pkl:base.String property surface (api/string facts
// batch). `isBase64`, `lastIndex`, `isRegex`, `base64Decoded`,
// `base64DecodedBytes` are all bare property reads in Apple Pkl.
| "isBase64"
| "lastIndex"
| "isRegex"
| "isGlobPattern"
| "base64Decoded"
| "base64DecodedBytes" => true
_ => false
}
}
///|
fn is_string_method_name(name : String) -> Bool {
match name {
"toUpperCase"
| "toLowerCase"
| "contains"
| "startsWith"
| "endsWith"
| "indexOf"
| "replaceAll"
| "replaceFirst"
| "take"
| "drop"
| "split"
| "padStart"
| "padEnd"
| "toBytes"
| "encodeToBytes"
| "codePointAt"
// Apple Pkl exposes the no-arg surface (`reverse`, `isEmpty`,
// `length`, etc.) as both property reads and method calls.
| "length"
| "isEmpty"
| "isNotEmpty"
| "isBlank"
| "isNotBlank"
| "reverse"
| "base64"
| "md5"
| "sha1"
| "sha256"
| "sha256Int"
| "codePointCount"
| "codePoints"
| "chars"
// PKL-149: extended pkl:base.String method surface for the
// api/string.pkl fact/example matrix. All five trim/case helpers,
// numeric conversions (toInt / toFloat / toBoolean and their
// `*OrNull` siblings), regex match (`matches`), `repeat`,
// substring/get bounds-safe siblings, and three more replace
// variants (`replaceLast`, `replaceAllMapped`,
// `replaceFirstMapped`, `replaceLastMapped`, `replaceRange`).
// `indexOfOrNull` / `lastIndexOf` / `lastIndexOfOrNull` /
// `splitLimit` round out the search surface. Also covers the
// bare-property names that Apple Pkl accepts as zero-arg method
// calls (`isBase64()` / `lastIndex()` etc).
| "matches"
| "capitalize"
| "decapitalize"
| "repeat"
| "trim"
| "trimStart"
| "trimEnd"
| "toBoolean"
| "toBooleanOrNull"
| "toInt"
| "toIntOrNull"
| "toFloat"
| "toFloatOrNull"
| "substring"
| "substringOrNull"
| "getOrNull"
| "replaceLast"
| "replaceRange"
| "replaceAllMapped"
| "replaceFirstMapped"
| "replaceLastMapped"
| "indexOfOrNull"
| "lastIndexOf"
| "lastIndexOfOrNull"
| "splitLimit"
| "takeWhile"
| "takeLast"
| "takeLastWhile"
| "dropWhile"
| "dropLast"
| "dropLastWhile"
| "isBase64"
| "lastIndex"
| "isRegex"
| "isGlobPattern"
| "base64Decoded"
| "base64DecodedBytes" => true
_ => false
}
}
///|
fn is_int_property_name(name : String) -> Bool {
match name {
"abs"
| "isEven"
| "isOdd"
| "isPositive"
| "isNonZero"
| "isFinite"
| "isNaN"
| "isInfinite"
| "sign"
| "inv"
// PKL-150: Int.ceil / .floor / .round / .truncate are no-ops on
// an integer (they round to the nearest Int, which is `self`).
// Apple Pkl projects them as both property reads and `()` calls.
| "ceil"
| "floor"
| "round"
| "truncate" => true
_ => false
}
}
///|
fn is_int_method_name(name : String) -> Bool {
match name {
// PKL-148b: pkl:base Int method surface adds `isBetween(a, b)`,
// `toFloat()`, `shl` / `shr` / `ushr` / `or` / `and` / `xor`.
"toString"
| "toChar"
| "toFloat"
| "isBetween"
// PKL-150: Int method surface — radix-string / fixed-decimal /
// `toInt()` (no-op) / `toDuration(unit)` / `toDataSize(unit)` /
// bitwise binary methods.
| "toInt"
| "toRadixString"
| "toFixed"
| "toDuration"
| "toDataSize"
| "shl"
| "shr"
| "ushr"
| "and"
| "or"
| "xor"
// No-arg property surface also accessible as `(N).method()`.
| "abs"
| "isEven"
| "isOdd"
| "isPositive"
| "isNonZero"
| "isFinite"
| "isNaN"
| "isInfinite"
| "sign"
| "inv"
| "ceil"
| "floor"
| "round"
| "truncate" => true
_ => false
}
}
///|
/// PKL-152: pkl:base.Typed method surface — `getProperty(name)`,
/// `getPropertyOrNull(name)`, `hasProperty(name)`, `toMap()`,
/// `toDynamic()`. These dispatch off any ObjectValue receiver via
/// the eval_callable_call intercept.
fn is_typed_method_name(name : String) -> Bool {
match name {
"getProperty"
| "getPropertyOrNull"
| "hasProperty"
| "toMap"
| "toDynamic"
// PKL-152: Dynamic-specific introspection — `length` (number of
// bare-element members), `toList` (project elements into a List),
// `toTyped()` (cast a Dynamic into a Typed instance with
// the supplied class).
| "length"
| "toList"
| "toTyped" => true
_ => false
}
}
///|
fn eval_universal_value_method(
value : Value,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match method_name {
"toString" => {
if arguments.length() != 0 {
diagnostics.push(
diag("Any.toString expects 0 arguments, got \{arguments.length()}"),
)
return None
}
Some(
StringValue(
dispatch_value_to_string(
value, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
),
),
)
}
"ifNonNull" => {
if arguments.length() != 1 {
diagnostics.push(
diag("Any.ifNonNull expects 1 argument, got \{arguments.length()}"),
)
return None
}
match value {
NullValue => Some(NullValue)
_ =>
match
eval_expr_with_bindings(
arguments[0],
bindings,
env,
class_env,
cache,
stack,
declarations,
diagnostics,
resolve_import,
) {
Some(callback) =>
apply_function_value(
"Any.ifNonNull callback",
callback,
[value],
bindings,
env,
class_env,
cache,
stack,
declarations,
diagnostics,
resolve_import,
)
None => None
}
}
}
_ => None
}
}
///|
fn class_mirror_simple_name(value : Value) -> String? {
match value {
ObjectValue(members) =>
match lookup_member(members, "simpleName") {
Some(StringValue(name)) => Some(name)
_ => None
}
_ => None
}
}
///|
fn class_mirror_display_name(value : Value) -> String? {
match value {
ObjectValue(members) =>
match lookup_member(members, "name") {
Some(StringValue(name)) => Some(name)
_ => class_mirror_simple_name(value)
}
_ => None
}
}
///|
fn user_class_is_abstract(
name : String,
declarations : Array[Declaration],
) -> Bool {
for declaration in declarations {
match declaration {
ClassDeclaration(class_decl) =>
if class_decl.name == name {
return class_decl.is_abstract
}
_ => ()
}
}
false
}
///|
fn typed_target_rejection_message(
class_name : String,
display_name : String,
class_env : Array[ClassBinding],
declarations : Array[Declaration],
) -> String? {
if is_abstract_class_name(class_name) ||
user_class_is_abstract(class_name, declarations) {
return Some("Cannot instantiate abstract class `\{display_name}`.")
}
match lookup_class_binding(class_env, class_name) {
Some(_) => None
None => Some("Class `\{display_name}` is not a subtype of `Typed`.")
}
}
///|
fn lookup_dynamic_string_entry(
members : Array[ValueMember],
name : String,
) -> Value? {
for field in members {
if !field.name.has_prefix("@subscript$") {
continue
}
match field.value {
ObjectValue(pair_members) =>
match
(
lookup_member(pair_members, "@key"),
lookup_member(pair_members, "@value"),
) {
(Some(StringValue(key)), Some(value)) if key == name =>
return Some(value)
_ => ()
}
_ => ()
}
}
None
}
///|
fn lookup_dynamic_typed_input(
members : Array[ValueMember],
name : String,
) -> Value? {
match lookup_visible_member(members, name) {
Some(value) => Some(value)
None => lookup_dynamic_string_entry(members, name)
}
}
///|
fn map_entries_to_dynamic_members(
entries : Array[ValueEntry],
) -> Array[ValueMember] {
let members : Array[ValueMember] = []
for entry in entries {
match entry.key {
StringValue(name) =>
members.push({ name, value: entry.value, source: None, annotations: [] })
_ =>
members.push({
name: "@subscript$map",
value: ObjectValue([
{ name: "@key", value: entry.key, source: None, annotations: [] },
{
name: "@value",
value: entry.value,
source: None,
annotations: [],
},
]),
source: None,
annotations: [],
})
}
}
members
}
///|
fn dynamic_members_to_typed_value(
receiver_members : Array[ValueMember],
class_name : String,
display_name : String,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match
typed_target_rejection_message(
class_name, display_name, class_env, declarations,
) {
Some(message) => {
diagnostics.push(diag(message))
return None
}
None => ()
}
let properties : Array[ClassProperty] = []
collect_class_properties_in_order(
properties, class_name, class_env, declarations,
)
let defaults = eval_class_default_members(
class_name, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
)
let typed_members : Array[ValueMember] = [
{
name: hidden_member_name("__class"),
value: StringValue(class_name),
source: None,
annotations: [],
},
]
for property in properties {
match lookup_dynamic_typed_input(receiver_members, property.name) {
Some(value) =>
typed_members.push({
name: property.name,
value,
source: None,
annotations: property.annotations,
})
None =>
match lookup_member(defaults, property.name) {
Some(value) =>
typed_members.push({
name: property.name,
value,
source: None,
annotations: property.annotations,
})
None =>
typed_members.push({
name: error_member_name(property.name),
value: StringValue(
"Tried to read property `\{property.name}` but its value is undefined.",
),
source: None,
annotations: [],
})
}
}
}
Some(ObjectValue(typed_members))
}
///|
/// PKL-152: dispatch a pkl:base.Typed method against a user-class
/// ObjectValue receiver. Each arm validates argument count locally;
/// missing properties surface the module-qualified diagnostic, in
/// keeping with `api/typed`'s gold wording.
fn eval_typed_method_dispatch(
receiver_members : Array[ValueMember],
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match method_name {
"getProperty" | "getPropertyOrNull" | "hasProperty" => {
if arguments.length() != 1 {
diagnostics.push(
diag("\{method_name} expects 1 argument, got \{arguments.length()}"),
)
return None
}
let name_value = eval_expr_with_bindings(
arguments[0],
bindings,
env,
class_env,
cache,
stack,
declarations,
diagnostics,
resolve_import,
)
match name_value {
Some(StringValue(name)) => {
let mut found : Value? = None
for m in receiver_members {
if !is_invisible_member_name(m.name) && m.name == name {
found = Some(m.value)
break
}
}
match (method_name, found) {
("getProperty", Some(v)) => Some(v)
("getProperty", None) => {
let class_tag = match find_object_class_tag(receiver_members) {
Some(s) => s
None => "Dynamic"
}
let qualified = if is_stdlib_class_name(class_tag) {
class_tag
} else {
let module_name = match lookup_value(cache, "@__module_name") {
Some(StringValue(s)) => s
_ => ""
}
if module_name.length() > 0 {
"\{module_name}#\{class_tag}"
} else {
class_tag
}
}
diagnostics.push(
diag(
"Cannot find property `\{name}` in object of type `\{qualified}`.",
),
)
None
}
("getPropertyOrNull", Some(v)) => Some(v)
("getPropertyOrNull", None) => Some(NullValue)
("hasProperty", Some(_)) => Some(BoolValue(true))
("hasProperty", None) =>
Some(
BoolValue(
lookup_pending_error_message(receiver_members, name)
is Some(_),
),
)
_ => None
}
}
Some(_) => {
diagnostics.push(diag("\{method_name} expects String argument"))
None
}
None => None
}
}
"toMap" => {
if arguments.length() != 0 {
diagnostics.push(
diag("toMap expects 0 arguments, got \{arguments.length()}"),
)
return None
}
let entries : Array[ValueEntry] = []
let is_module = match find_object_class_tag(receiver_members) {
Some("Module") => true
_ => false
}
for m in receiver_members {
if m.name.has_prefix("@subscript$") {
match m.value {
ObjectValue(pair_members) =>
match
(
lookup_member(pair_members, "@key"),
lookup_member(pair_members, "@value"),
) {
(Some(StringValue(_) as key), Some(value)) =>
entries.push({ key, value })
_ => ()
}
_ => ()
}
} else if m.name.has_prefix("@element$") {
continue
} else if !is_invisible_member_name(m.name) &&
!(is_module &&
(
m.name == "imports" ||
m.name == "output" ||
m.value is FunctionValue(_, _, _, _, _)
)) {
entries.push({ key: StringValue(m.name), value: m.value })
}
}
Some(MapValue(entries))
}
"toDynamic" => {
if arguments.length() != 0 {
diagnostics.push(
diag("toDynamic expects 0 arguments, got \{arguments.length()}"),
)
return None
}
let next : Array[ValueMember] = []
for m in receiver_members {
if m.name == hidden_member_name("__class") {
continue
}
next.push(m)
}
Some(ObjectValue(tag_object_with_class(next, "Dynamic")))
}
"length" => {
if arguments.length() != 0 {
diagnostics.push(
diag("length expects 0 arguments, got \{arguments.length()}"),
)
return None
}
// PKL-152: Dynamic.length counts only bare-element members
// (`@element$`). Properties and bracket-entries don't
// count, matching Apple Pkl's gold (`new Dynamic { name = ... }.length
// == 0`).
let mut count = 0
for m in receiver_members {
if m.name.has_prefix("@element$") {
count = count + 1
}
}
Some(IntValue(count.to_int64()))
}
"toList" => {
if arguments.length() != 0 {
diagnostics.push(
diag("toList expects 0 arguments, got \{arguments.length()}"),
)
return None
}
let out : Array[Value] = []
for m in receiver_members {
if m.name.has_prefix("@element$") {
out.push(m.value)
}
}
for m in receiver_members {
if !m.name.has_prefix("@subscript$") {
continue
}
match m.value {
ObjectValue(pair_members) =>
match
(
lookup_member(pair_members, "@key"),
lookup_member(pair_members, "@value"),
) {
(Some(IntValue(raw)), Some(value)) => {
let idx = raw.to_int()
if raw >= 0L && idx < out.length() {
out[idx] = value
}
}
_ => ()
}
_ => ()
}
}
Some(ListValue(out))
}
"toTyped" => {
if arguments.length() != 1 {
diagnostics.push(
diag("toTyped expects 1 argument, got \{arguments.length()}"),
)
return None
}
// PKL-152: Dynamic.toTyped() stamps the receiver's
// visible members with the supplied class's tag, applies class
// defaults, drops extras, and keeps abstract slots lazy.
match
eval_expr_with_bindings(
arguments[0],
bindings,
env,
class_env,
cache,
stack,
declarations,
diagnostics,
resolve_import,
) {
Some(class_value) =>
match
(
class_mirror_simple_name(class_value),
class_mirror_display_name(class_value),
) {
(Some(class_name), Some(display_name)) =>
dynamic_members_to_typed_value(
receiver_members, class_name, display_name, bindings, env, class_env,
cache, stack, declarations, diagnostics, resolve_import,
)
_ => {
diagnostics.push(diag("toTyped expects a Class mirror argument"))
None
}
}
_ => {
diagnostics.push(diag("toTyped expects a Class mirror argument"))
None
}
}
}
_ => None
}
}
///|
/// PKL-148: pkl:base Boolean method names.
fn is_bool_method_name(name : String) -> Bool {
match name {
"xor" | "implies" | "and" | "or" | "toString" => true
_ => false
}
}
///|
/// PKL-148: pkl:base Float method names. Mirrors `is_int_method_name`
/// but routes through `eval_float_method`.
fn is_float_method_name(name : String) -> Bool {
match name {
"toString"
| "toFloat"
| "toInt"
| "toFixed"
| "toDuration"
| "toDataSize"
| "isBetween"
| "abs"
| "round"
| "floor"
| "ceil"
| "truncate"
// No-arg property surface — callable as `(x).isNaN()` etc.
| "isPositive"
| "isNonZero"
| "isFinite"
| "isNaN"
| "isInfinite"
| "sign" => true
_ => false
}
}
///|
fn is_duration_unit_name(name : String) -> Bool {
match name {
"ns" | "us" | "ms" | "s" | "min" | "h" | "d" => true
_ => false
}
}
///|
fn is_datasize_unit_name(name : String) -> Bool {
match name {
"b"
| "kb"
| "kib"
| "mb"
| "mib"
| "gb"
| "gib"
| "tb"
| "tib"
| "pb"
| "pib" => true
_ => false
}
}
///|
/// Position of a Duration unit on the ns..d ladder. Returned values are
/// only used for ordering and adjacent-step lookups — the absolute
/// factor between two units is reconstructed by walking the ladder a
/// step at a time so the intermediate magnitude never overflows Int32
/// (a flat `min`-to-ns factor would already need ≥ 6e10).
fn duration_unit_level(unit : String) -> Int {
match unit {
"ns" => 0
"us" => 1
"ms" => 2
"s" => 3
"min" => 4
"h" => 5
"d" => 6
_ => -1
}
}
///|
/// Integer factor that scales one unit at `level` to the next-finer unit
/// at `level - 1` (e.g. `min` → `s` is 60, `s` → `ms` is 1000).
fn duration_step_factor(level : Int) -> Int {
match level {
1 => 1000 // us -> ns
2 => 1000 // ms -> us
3 => 1000 // s -> ms
4 => 60 // min -> s
5 => 60 // h -> min
6 => 24 // d -> h
_ => 1
}
}
///|
/// Factor from a DataSize unit to bytes. Stored as Double so `tb` / `pb`
/// and their binary siblings can participate in `toUnit` and comparisons
/// without overflowing MoonBit's Int.
fn datasize_byte_factor(unit : String) -> Double {
match unit {
"b" => 1.0
"kb" => 1000.0
"kib" => 1024.0
"mb" => 1_000_000.0
"mib" => 1_048_576.0
"gb" => 1_000_000_000.0
"gib" => 1_073_741_824.0
"tb" => 1_000_000_000_000.0
"tib" => 1_099_511_627_776.0
"pb" => 1_000_000_000_000_000.0
"pib" => 1_125_899_906_842_624.0
_ => -1.0
}
}
///|
fn is_datasize_binary_unit(unit : String) -> Bool {
match unit {
"b" | "kib" | "mib" | "gib" | "tib" | "pib" => true
_ => false
}
}
///|
fn is_datasize_decimal_unit(unit : String) -> Bool {
match unit {
"b" | "kb" | "mb" | "gb" | "tb" | "pb" => true
_ => false
}
}
///|
fn datasize_binary_unit_for(unit : String) -> String {
match unit {
"kb" | "kib" => "kib"
"mb" | "mib" => "mib"
"gb" | "gib" => "gib"
"tb" | "tib" => "tib"
"pb" | "pib" => "pib"
_ => "b"
}
}
///|
fn datasize_decimal_unit_for(unit : String) -> String {
match unit {
"kb" | "kib" => "kb"
"mb" | "mib" => "mb"
"gb" | "gib" => "gb"
"tb" | "tib" => "tb"
"pb" | "pib" => "pb"
_ => "b"
}
}
///|
/// Pick the unit with the larger magnitude-per-unit. Apple Pkl renders
/// mixed Duration / DataSize arithmetic in the coarser of the two units.
fn larger_duration_unit(a : String, b : String) -> String {
if duration_unit_level(a) >= duration_unit_level(b) {
a
} else {
b
}
}
///|
fn larger_datasize_unit(a : String, b : String) -> String {
let fa = datasize_byte_factor(a)
let fb = datasize_byte_factor(b)
if fa >= fb {
a
} else {
b
}
}
///|
/// Convert `magnitude` from `from_unit` to `to_unit` by walking one step
/// along the unit ladder at a time. Coarser→finer multiplies by each
/// step factor; finer→coarser divides (truncating). Stepping avoids the
/// overflow that a flat `factor_to_ns / factor_to_target` would hit on
/// `min`, `h`, or `d`.
fn duration_in_unit(
magnitude : Double,
from_unit : String,
to_unit : String,
) -> Double {
let from = duration_unit_level(from_unit)
let to = duration_unit_level(to_unit)
let mut m = magnitude
let mut level = from
while level > to {
m = m * duration_step_factor(level).to_double()
level = level - 1
}
while level < to {
m = m / duration_step_factor(level + 1).to_double()
level = level + 1
}
m
}
///|
fn duration_literal_text(magnitude : Double, unit : String) -> String {
"\{magnitude}.\{unit}"
}
///|
fn left_pad_9_digits(value : Int64) -> String {
let raw = value.to_string()
let buf = StringBuilder::new()
let mut i = raw.length()
while i < 9 {
buf.write_char('0')
i = i + 1
}
buf.write_string(raw)
buf.to_string()
}
///|
fn trim_trailing_zeroes(text : String) -> String {
let mut end = text.length()
while end > 0 && text[end - 1].to_int().unsafe_to_char() == '0' {
end = end - 1
}
String::unsafe_substring(text, start=0, end~)
}
///|
fn duration_second_component_text(seconds_ns : Int64) -> String {
let ns_per_second = 1_000_000_000L
let seconds = seconds_ns / ns_per_second
let fractional = seconds_ns % ns_per_second
if fractional == 0L {
seconds.to_string()
} else {
seconds.to_string() +
"." +
trim_trailing_zeroes(left_pad_9_digits(fractional))
}
}
///|
fn duration_iso_string(
magnitude : Double,
unit : String,
diagnostics : Array[Diagnostic],
) -> String? {
if magnitude.is_nan() || magnitude.is_inf() {
diagnostics.push(
diag(
"Cannot convert duration `\{duration_literal_text(magnitude, unit)}` to ISO 8601 duration.",
),
)
return None
}
let seconds = duration_in_unit(magnitude, unit, "s")
let negative = seconds < 0.0
let abs_seconds = if negative { 0.0 - seconds } else { seconds }
if abs_seconds > 9_000_000_000_000_000_000.0 {
if negative {
return Some("-PT9223372036854775807H7M8S")
}
return Some("PT9223372036854775807H7M8S")
}
let total_ns = (duration_in_unit(abs_seconds, "s", "ns") + 0.5).to_int64()
let ns_per_second = 1_000_000_000L
let ns_per_minute = 60L * ns_per_second
let ns_per_hour = 60L * ns_per_minute
let hours = total_ns / ns_per_hour
let after_hours = total_ns % ns_per_hour
let minutes = after_hours / ns_per_minute
let seconds_ns = after_hours % ns_per_minute
let buf = StringBuilder::new()
if negative {
buf.write_char('-')
}
buf.write_string("PT")
if hours > 0L {
buf.write_string(hours.to_string())
buf.write_char('H')
}
if minutes > 0L {
buf.write_string(minutes.to_string())
buf.write_char('M')
}
if seconds_ns > 0L || (hours == 0L && minutes == 0L) {
buf.write_string(duration_second_component_text(seconds_ns))
buf.write_char('S')
}
Some(buf.to_string())
}
///|
/// Convert `magnitude` from `from_unit` to `to_unit` for DataSize.
fn datasize_in_unit(
magnitude : Double,
from_unit : String,
to_unit : String,
) -> Double {
let from_f = datasize_byte_factor(from_unit)
let to_f = datasize_byte_factor(to_unit)
if from_f < 0.0 || to_f < 0.0 {
return magnitude
}
if from_f == to_f {
magnitude
} else {
magnitude * from_f / to_f
}
}
///|
fn is_regex_method_name(name : String) -> Bool {
match name {
"matches"
| "find"
| "findAll"
| "findMatchesIn"
| "matchEntire"
| "replace"
| "replaceAll" => true
_ => false
}
}
///|
fn is_bytes_method_name(name : String) -> Bool {
match name {
"getOrNull" | "toList" | "decodeToString" => true
_ => false
}
}
///|
/// Build a `BytesValue` from a Listing of Int byte values. Each entry
/// must be in the 0..=255 range; an out-of-range or non-Int element
/// records a diagnostic and bails. The resulting `Bytes` is owned by
/// the new `BytesValue` and shared by reference for the lifetime of
/// the value (MoonBit `Bytes` is immutable).
fn build_bytes_from_int_listing(
elements : Array[Value],
diagnostics : Array[Diagnostic],
) -> Value? {
let buf : Array[Byte] = []
for element in elements {
match element {
IntValue(n) =>
if n < 0 || n > 255 {
diagnostics.push(
diag("Type constraint `isBetween(0, 255)` violated. Value: \{n}"),
)
return None
} else {
buf.push(n.to_byte())
}
_ => {
diagnostics.push(
diag(
"Expected value of type `Int`, but got `\{render_pcf_value_inline(element)}`.",
),
)
return None
}
}
}
Some(BytesValue(Bytes::from_array(buf)))
}
///|
fn eval_bytes_property(
bytes : Bytes,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"length" => Some(IntValue(bytes.length().to_int64()))
"base64" => Some(StringValue(@base64.encode(bytes[:])))
"hex" => Some(StringValue(@crypto.bytes_to_hex_string(bytes)))
"md5" => Some(StringValue(@crypto.bytes_to_hex_string(@crypto.md5(bytes))))
"sha1" =>
Some(StringValue(@crypto.bytes_to_hex_string(@crypto.sha1(bytes))))
"sha256" =>
Some(StringValue(@crypto.bytes_to_hex_string(@crypto.sha256(bytes))))
"sha256Int" => Some(IntValue(sha256_int_for_bytes(bytes)))
"size" => Some(bytes_size_value(bytes))
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `Bytes`."),
)
None
}
}
}
///|
fn eval_bytes_method(
bytes : Bytes,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"decodeToString" =>
if arg_values.length() == 0 ||
(arg_values.length() == 1 && arg_values[0] == StringValue("UTF-8")) {
Some(StringValue(@utf8.decode(bytes[:]))) catch {
_ => {
diagnostics.push(diag("Could not decode bytes as UTF-8."))
None
}
}
} else {
diagnostics.push(diag("Bytes.decodeToString expects UTF-8 encoding"))
None
}
"getOrNull" =>
if arg_values.length() == 1 {
match arg_values[0] {
IntValue(index64) => {
let index = index64.to_int()
if index64 < 0L || index >= bytes.length() {
Some(NullValue)
} else {
Some(IntValue(bytes[index].to_int().to_int64()))
}
}
_ => {
diagnostics.push(diag("Bytes.getOrNull expects an Int argument"))
None
}
}
} else {
diagnostics.push(diag("Bytes.getOrNull expects exactly one argument"))
None
}
"toList" =>
if arg_values.length() == 0 {
let elements : Array[Value] = []
for i = 0; i < bytes.length(); i = i + 1 {
elements.push(IntValue(bytes[i].to_int().to_int64()))
}
Some(ListValue(elements))
} else {
diagnostics.push(diag("Bytes.toList takes no arguments"))
None
}
_ => {
diagnostics.push(
diag("Cannot find method `\{method_name}` in object of type `Bytes`."),
)
None
}
}
}
///|
fn bytes_size_value(bytes : Bytes) -> Value {
let len = bytes.length()
if len > 0 && len % 1000 == 0 {
DataSizeValue((len / 1000).to_double(), "kb")
} else {
DataSizeValue(len.to_double(), "b")
}
}
///|
fn sha256_int_for_bytes(bytes : Bytes) -> Int64 {
let digest = @crypto.sha256(bytes)
let mut acc = 0L
for i = 0; i < 8; i = i + 1 {
acc = acc.lor(digest[i].to_int().to_int64() << (i * 8))
}
acc
}
///|
fn resource_property_name(name : String) -> Bool {
match name {
"text" | "base64" | "bytes" | "md5" | "sha1" | "sha256" | "sha256Int" =>
true
_ => false
}
}
///|
fn resource_bytes(
members : Array[ValueMember],
diagnostics : Array[Diagnostic],
) -> Bytes? {
match lookup_visible_member(members, "bytes") {
Some(BytesValue(bytes)) => return Some(bytes)
Some(_) => {
diagnostics.push(diag("Resource.bytes must be Bytes."))
return None
}
None => ()
}
match lookup_visible_member(members, "base64") {
Some(StringValue(encoded)) =>
Some(@base64.decode(encoded[:])) catch {
_ => {
diagnostics.push(diag("Could not base64-decode resource."))
None
}
}
Some(_) => {
diagnostics.push(diag("Resource.base64 must be String."))
None
}
None =>
match lookup_visible_member(members, "text") {
Some(StringValue(text)) => Some(@utf8.encode(text))
Some(_) => {
diagnostics.push(diag("Resource.text must be String."))
None
}
None => Some(b"")
}
}
}
///|
fn eval_resource_property(
members : Array[ValueMember],
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"text" =>
match lookup_visible_member(members, "text") {
Some(value) => Some(value)
None => {
diagnostics.push(
diag("Cannot find property `text` in object of type `Resource`."),
)
None
}
}
"base64" =>
match lookup_visible_member(members, "base64") {
Some(value) => Some(value)
None =>
match resource_bytes(members, diagnostics) {
Some(bytes) => Some(StringValue(@base64.encode(bytes[:])))
None => None
}
}
"bytes" =>
match resource_bytes(members, diagnostics) {
Some(bytes) => Some(BytesValue(bytes))
None => None
}
"md5" =>
match resource_bytes(members, diagnostics) {
Some(bytes) =>
Some(StringValue(@crypto.bytes_to_hex_string(@crypto.md5(bytes))))
None => None
}
"sha1" =>
match resource_bytes(members, diagnostics) {
Some(bytes) =>
Some(StringValue(@crypto.bytes_to_hex_string(@crypto.sha1(bytes))))
None => None
}
"sha256" =>
match resource_bytes(members, diagnostics) {
Some(bytes) =>
Some(StringValue(@crypto.bytes_to_hex_string(@crypto.sha256(bytes))))
None => None
}
"sha256Int" =>
match resource_bytes(members, diagnostics) {
Some(bytes) => Some(IntValue(sha256_int_for_bytes(bytes)))
None => None
}
_ => None
}
}
///|
/// Compile `pattern` via `moonbitlang/regexp`. Returns `None` and pushes
/// a diagnostic on syntax errors so callers can bail out without
/// propagating a raise. Each call recompiles — the value model only
/// caches the source string so equality stays decidable on the surface.
fn compile_regex_pattern(
pattern : String,
diagnostics : Array[Diagnostic],
) -> @regexp.Regexp? {
Some(@regexp.compile(pattern)) catch {
err => {
diagnostics.push(diag(regex_syntax_error_message(pattern, "\{err}")))
None
}
}
}
///|
fn regex_syntax_error_message(pattern : String, err : String) -> String {
if pattern == "(" || err.contains("MissingParenthesis") {
"Syntax error in regex `\{pattern}`: Unclosed group near index \{pattern.length()} \{pattern}"
} else if pattern == "*" {
"Syntax error in regex `*`: Dangling meta character '*' near index 0 * ^"
} else {
"Syntax error in regex `\{pattern}`: \{err}"
}
}
///|
fn regex_matches_full(re : @regexp.Regexp, input : String) -> Bool {
let m = re.execute(input)
if !m.matched() {
return false
}
// Full-input match: the captured slice must cover the entire input
// with nothing before or after. `before` / `after` come back as views
// so an empty view means the match anchored at both ends.
m.before().length() == 0 && m.after().length() == 0
}
///|
fn regex_find_first(re : @regexp.Regexp, input : String) -> String? {
match re.match_(input) {
Some(m) =>
match m.get(0) {
Some(view) => Some(view.to_owned())
None => None
}
None => None
}
}
///|
/// Repeatedly execute the regex on the remaining suffix, collecting each
/// match's text. Advances by at least one character on zero-width
/// matches so the walk always terminates.
fn regex_find_all(re : @regexp.Regexp, input : String) -> Array[String] {
let out : Array[String] = []
let mut rest = input
while rest.length() > 0 {
match re.match_(rest) {
Some(m) =>
match m.get(0) {
Some(view) => {
let matched_text = view.to_owned()
out.push(matched_text)
let after = m.after().to_owned()
if matched_text.length() == 0 && after.length() == rest.length() {
// Zero-width match at the head of `rest`: skip one char to
// avoid an infinite loop.
if rest.length() <= 1 {
rest = ""
} else {
rest = String::unsafe_substring(
rest,
start=1,
end=rest.length(),
)
}
} else {
rest = after
}
}
None => break
}
None => break
}
}
out
}
///|
fn regex_capture_group_count(pattern : String) -> Int {
let mut count = 0
let mut escaped = false
let mut in_class = false
let mut i = 0
while i < pattern.length() {
let ch = pattern[i]
if escaped {
escaped = false
i = i + 1
continue
}
if ch == '\\' {
escaped = true
i = i + 1
continue
}
if in_class {
if ch == ']' {
in_class = false
}
i = i + 1
continue
}
if ch == '[' {
in_class = true
i = i + 1
continue
}
if ch == '(' {
if i + 1 < pattern.length() && pattern[i + 1] == '?' {
if i + 2 < pattern.length() && pattern[i + 2] == '<' {
count = count + 1
}
} else {
count = count + 1
}
}
i = i + 1
}
count
}
///|
fn regex_match_object(
re : @regexp.Regexp,
match_result : @regexp.MatchResult,
offset : Int,
) -> Value {
let groups : Array[Value] = []
for i = 0; i < re.group_count(); i = i + 1 {
match match_result.get(i) {
Some(view) =>
groups.push(regex_match_group_object(view, offset, ListValue([])))
None => groups.push(NullValue)
}
}
match match_result.get(0) {
Some(view) => regex_match_group_object(view, offset, ListValue(groups))
None =>
ObjectValue([
{ name: "value", value: StringValue(""), source: None, annotations: [] },
{
name: "start",
value: IntValue(offset.to_int64()),
source: None,
annotations: [],
},
{
name: "end",
value: IntValue(offset.to_int64()),
source: None,
annotations: [],
},
{
name: "groups",
value: ListValue(groups),
source: None,
annotations: [],
},
])
}
}
///|
fn regex_match_group_object(
view : StringView,
offset : Int,
groups : Value,
) -> Value {
let start = offset + view.start_offset()
ObjectValue([
{
name: "value",
value: StringValue(view.to_owned()),
source: None,
annotations: [],
},
{
name: "start",
value: IntValue(start.to_int64()),
source: None,
annotations: [],
},
{
name: "end",
value: IntValue((start + view.length()).to_int64()),
source: None,
annotations: [],
},
{ name: "groups", value: groups, source: None, annotations: [] },
])
}
///|
fn regex_find_match_objects(
re : @regexp.Regexp,
input : String,
) -> Array[Value] {
let out : Array[Value] = []
let mut rest = input
let mut offset = 0
while offset <= input.length() {
match re.match_(rest) {
Some(match_result) =>
match match_result.get(0) {
Some(view) => {
out.push(regex_match_object(re, match_result, offset))
let before_len = match_result.before().length()
let matched_len = view.length()
let advance_tail = if matched_len == 0 { 1 } else { matched_len }
let advance = before_len + advance_tail
if advance == 0 || offset + advance > input.length() {
break
}
offset = offset + advance
rest = String::unsafe_substring(
input,
start=offset,
end=input.length(),
)
}
None => break
}
None => break
}
}
out
}
///|
fn regex_replace_first(
re : @regexp.Regexp,
input : String,
replacement : String,
) -> String {
match re.match_(input) {
Some(m) => {
let buf = StringBuilder::new()
buf.write_string(m.before().to_owned())
buf.write_string(replacement)
buf.write_string(m.after().to_owned())
buf.to_string()
}
None => input
}
}
///|
fn regex_replace_all(
re : @regexp.Regexp,
input : String,
replacement : String,
) -> String {
let buf = StringBuilder::new()
let mut rest = input
while rest.length() > 0 {
match re.match_(rest) {
Some(m) =>
match m.get(0) {
Some(view) => {
let matched_text = view.to_owned()
buf.write_string(m.before().to_owned())
buf.write_string(replacement)
let after = m.after().to_owned()
if matched_text.length() == 0 && after.length() == rest.length() {
// Zero-width match: emit the first char verbatim then
// continue, so substitution never spins.
if rest.length() >= 1 {
buf.write_string(String::unsafe_substring(rest, start=0, end=1))
}
if rest.length() <= 1 {
rest = ""
} else {
rest = String::unsafe_substring(
rest,
start=1,
end=rest.length(),
)
}
} else {
rest = after
}
}
None => {
buf.write_string(rest)
rest = ""
}
}
None => {
buf.write_string(rest)
rest = ""
}
}
}
buf.to_string()
}
///|
fn eval_int_property(
n : Int64,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"abs" => if n < 0L { Some(IntValue(0L - n)) } else { Some(IntValue(n)) }
"isEven" => Some(BoolValue(n % 2L == 0L))
"isOdd" => Some(BoolValue(n % 2L != 0L))
// PKL-148: pkl:base Int zero-arg accessors.
"isPositive" => Some(BoolValue(n >= 0L))
"isNonZero" => Some(BoolValue(n != 0L))
"isFinite" => Some(BoolValue(true))
"isNaN" => Some(BoolValue(false))
"isInfinite" => Some(BoolValue(false))
"sign" =>
if n > 0L {
Some(IntValue(1L))
} else if n < 0L {
Some(IntValue(-1L))
} else {
Some(IntValue(0L))
}
"inv" => Some(IntValue(n.lnot()))
// PKL-150: rounding / truncation are no-ops on Int.
"ceil" | "floor" | "round" | "truncate" => Some(IntValue(n))
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `Int`."),
)
None
}
}
}
///|
/// Float counterpart of `eval_int_property`. Apple Pkl exposes the
/// same no-arg surface (`abs`, `isPositive`, `isNaN`, …) on Float.
fn eval_float_property(
d : Double,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"abs" => Some(FloatValue(if d < 0.0 { 0.0 - d } else { d.abs() }))
"ceil" => Some(FloatValue(d.ceil()))
"floor" => Some(FloatValue(d.floor()))
"isPositive" => Some(BoolValue(d >= 0.0))
"isNonZero" => Some(BoolValue(d != 0.0))
"isFinite" => Some(BoolValue(!d.is_nan() && !d.is_inf()))
"isNaN" => Some(BoolValue(d.is_nan()))
"isInfinite" => Some(BoolValue(d.is_inf()))
"sign" =>
if d.is_nan() {
Some(FloatValue(d))
} else if is_negative_zero(d) {
Some(FloatValue(d))
} else if d > 0.0 {
Some(FloatValue(1.0))
} else if d < 0.0 {
Some(FloatValue(-1.0))
} else {
Some(FloatValue(0.0))
}
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `Float`."),
)
None
}
}
}
///|
fn is_float_property_name(name : String) -> Bool {
match name {
"abs"
| "ceil"
| "floor"
| "isPositive"
| "isNonZero"
| "isFinite"
| "isNaN"
| "isInfinite"
| "sign" => true
_ => false
}
}
///|
fn eval_duration_property(
magnitude : Double,
unit : String,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"value" => Some(magnitude_as_value(magnitude))
"isoString" =>
match duration_iso_string(magnitude, unit, diagnostics) {
Some(text) => Some(StringValue(text))
None => None
}
"unit" => Some(StringValue(unit))
"isPositive" => Some(BoolValue(magnitude >= 0.0))
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `Duration`."),
)
None
}
}
}
///|
fn eval_datasize_property(
magnitude : Double,
unit : String,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"value" => Some(magnitude_as_value(magnitude))
"unit" => Some(StringValue(unit))
"isPositive" => Some(BoolValue(magnitude >= 0.0))
"isBinaryUnit" => Some(BoolValue(is_datasize_binary_unit(unit)))
"isDecimalUnit" => Some(BoolValue(is_datasize_decimal_unit(unit)))
"toBinaryUnit" => {
let target = datasize_binary_unit_for(unit)
Some(DataSizeValue(datasize_in_unit(magnitude, unit, target), target))
}
"toDecimalUnit" => {
let target = datasize_decimal_unit_for(unit)
Some(DataSizeValue(datasize_in_unit(magnitude, unit, target), target))
}
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `DataSize`."),
)
None
}
}
}
///|
/// PKL-121: project a magnitude back to an `IntValue` when it has no
/// fractional component and fits in `Int`, otherwise expose it as
/// `FloatValue`. Keeps the common Int-magnitude path observationally
/// identical to the pre-PKL-121 behaviour (`.value` on `5.min` is
/// still `5`, not `5.0`).
fn magnitude_as_value(magnitude : Double) -> Value {
let rounded = magnitude.to_int64()
if rounded.to_double() == magnitude {
IntValue(rounded)
} else {
FloatValue(magnitude)
}
}
///|
fn int_to_string_radix(n : Int64, radix : Int) -> String {
if n == 0L {
return "0"
}
let digits = "0123456789abcdefghijklmnopqrstuvwxyz"
let negative = n < 0L
// Keep the working value non-positive. `abs(minInt)` is not
// representable as Int64, whereas negating every non-negative value is.
let mut value = if negative { n } else { 0L - n }
let radix64 = radix.to_int64()
let chars : Array[Char] = []
while value < 0L {
let d = (0L - value % radix64).to_int()
chars.push(digits.unsafe_get(d).to_int().unsafe_to_char())
value = value / radix64
}
let buf = StringBuilder::new()
if negative {
buf.write_char('-')
}
let mut i = chars.length() - 1
while i >= 0 {
buf.write_char(chars[i])
i = i - 1
}
buf.to_string()
}
///|
fn eval_int_method(
n : Int64,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"toString" =>
if arg_values.length() == 0 {
Some(StringValue("\{n}"))
} else if arg_values.length() == 1 {
match arg_values[0] {
IntValue(radix) =>
if radix < 2L || radix > 36L {
diagnostics.push(
diag(
"Int.toString radix must be between 2 and 36, got \{radix}",
),
)
None
} else {
Some(StringValue(int_to_string_radix(n, radix.to_int())))
}
_ => {
diagnostics.push(diag("Int.toString radix must be Int"))
None
}
}
} else {
diagnostics.push(
diag(
"Int.toString expects 0 or 1 arguments, got \{arg_values.length()}",
),
)
None
}
"toChar" => {
if arg_values.length() != 0 {
diagnostics.push(
diag("Int.toChar expects 0 arguments, got \{arg_values.length()}"),
)
return None
}
if n < 0L || n > 0x10FFFFL {
// PKL-150: match Apple Pkl's wording — "Decimal `N` is not a
// valid Unicode code point." with a comma-grouped magnitude.
diagnostics.push(
diag(
"Decimal `\{format_int_with_commas(n)}` is not a valid Unicode code point.",
),
)
return None
}
let buf = StringBuilder::new()
buf.write_char(n.to_int().unsafe_to_char())
Some(StringValue(buf.to_string()))
}
// PKL-148b: Int.isBetween(lower, upper) — inclusive range check.
"isBetween" => {
if arg_values.length() != 2 {
diagnostics.push(
diag("Int.isBetween expects 2 arguments, got \{arg_values.length()}"),
)
return None
}
let lower = match arg_values[0] {
IntValue(v) => v.to_double()
FloatValue(v) => v
_ => {
diagnostics.push(diag("Int.isBetween expects Number arguments"))
return None
}
}
let upper = match arg_values[1] {
IntValue(v) => v.to_double()
FloatValue(v) => v
_ => {
diagnostics.push(diag("Int.isBetween expects Number arguments"))
return None
}
}
let v = n.to_double()
Some(BoolValue(v >= lower && v <= upper))
}
// PKL-148b: Int.toFloat() — widens to Float (no precision loss).
"toFloat" =>
if arg_values.length() == 0 {
Some(FloatValue(n.to_double()))
} else {
diagnostics.push(diag("Int.toFloat takes no arguments"))
None
}
// PKL-150: Int.toInt() — Apple Pkl projects it for symmetry with
// Float.toInt(); on Int it's the identity.
"toInt" =>
if arg_values.length() == 0 {
Some(IntValue(n))
} else {
diagnostics.push(diag("Int.toInt takes no arguments"))
None
}
// PKL-150: Int.toRadixString(radix) — base 2-36, raises out of
// range. Negative inputs render with a leading "-".
"toRadixString" => {
if arg_values.length() != 1 {
diagnostics.push(
diag(
"Int.toRadixString expects 1 argument, got \{arg_values.length()}",
),
)
return None
}
match arg_values[0] {
IntValue(radix) =>
if radix < 2L || radix > 36L {
// PKL-150: match Apple Pkl's wording — the parameter is
// declared `Int(this.isBetween(2, 36))` so the violation
// surfaces through the constraint message verbatim.
diagnostics.push(
diag(
"Type constraint `this.isBetween(2, 36)` violated. Value: \{radix}",
),
)
None
} else {
Some(StringValue(int_to_string_radix(n, radix.to_int())))
}
_ => {
diagnostics.push(diag("Int.toRadixString expects Int argument"))
None
}
}
}
// PKL-150: Int.toFixed(decimals) — fixed-point string with the
// exact requested number of trailing zeros. 0..20 is the Apple Pkl
// accepted range; outside raises.
"toFixed" => {
if arg_values.length() != 1 {
diagnostics.push(
diag("Int.toFixed expects 1 argument, got \{arg_values.length()}"),
)
return None
}
match arg_values[0] {
IntValue(decimals) =>
if decimals < 0L || decimals > 20L {
// PKL-150: Apple Pkl's `toFixed(decimals: Int(this.isBetween(0, 20)))`
// surfaces a constraint-violation message verbatim.
diagnostics.push(
diag(
"Type constraint `this.isBetween(0, 20)` violated. Value: \{decimals}",
),
)
None
} else {
Some(StringValue(format_fixed_decimal(n, decimals.to_int())))
}
_ => {
diagnostics.push(diag("Int.toFixed expects Int argument"))
None
}
}
}
// PKL-150: Int.toDuration(unit) / .toDataSize(unit) — wraps the
// magnitude as a Duration / DataSize with the supplied unit.
"toDuration" => {
if arg_values.length() != 1 {
diagnostics.push(
diag("Int.toDuration expects 1 argument, got \{arg_values.length()}"),
)
return None
}
match arg_values[0] {
StringValue(unit) =>
if is_duration_unit_name(unit) {
Some(DurationValue(n.to_double(), unit))
} else {
diagnostics.push(diag("Invalid Duration unit: \"\{unit}\"."))
None
}
_ => {
diagnostics.push(diag("Int.toDuration expects String argument"))
None
}
}
}
"toDataSize" => {
if arg_values.length() != 1 {
diagnostics.push(
diag("Int.toDataSize expects 1 argument, got \{arg_values.length()}"),
)
return None
}
match arg_values[0] {
StringValue(unit) =>
if is_datasize_unit_name(unit) {
Some(DataSizeValue(n.to_double(), unit))
} else {
diagnostics.push(diag("Invalid DataSize unit: \"\{unit}\"."))
None
}
_ => {
diagnostics.push(diag("Int.toDataSize expects String argument"))
None
}
}
}
// PKL-150: bitwise binary Int methods (Apple Pkl spells `shl` /
// `shr` / `ushr` / `and` / `or` / `xor` as methods, not operators).
"shl" | "shr" | "ushr" | "and" | "or" | "xor" => {
if arg_values.length() != 1 {
diagnostics.push(
diag(
"Int.\{method_name} expects 1 argument, got \{arg_values.length()}",
),
)
return None
}
match arg_values[0] {
IntValue(m) => {
// PKL-150: shift count is Int (i32). Bitwise ops are Int64
// and / or / xor.
let m_int = m.to_int()
match method_name {
"shl" => Some(IntValue(n << m_int))
"shr" => Some(IntValue(n >> m_int))
"ushr" =>
Some(
IntValue(
(n.reinterpret_as_uint64() >> m_int).reinterpret_as_int64(),
),
)
"and" => Some(IntValue(n.land(m)))
"or" => Some(IntValue(n.lor(m)))
"xor" => Some(IntValue(n.lxor(m)))
_ => None
}
}
_ => {
diagnostics.push(diag("Int.\{method_name} expects Int argument"))
None
}
}
}
// No-arg property surface accessible as `(N).abs()` / `.isEven()`
// / `.sign()` / etc.
"abs"
| "isEven"
| "isOdd"
| "isPositive"
| "isNonZero"
| "isFinite"
| "isNaN"
| "isInfinite"
| "sign"
| "inv"
| "ceil"
| "floor"
| "round"
| "truncate" =>
if arg_values.length() != 0 {
diagnostics.push(
diag(
"Int.\{method_name} expects 0 arguments, got \{arg_values.length()}",
),
)
None
} else {
eval_int_property(n, method_name, diagnostics)
}
_ => None
}
}
///|
/// PKL-150: format `n` as a fixed-point string with exactly
/// `decimals` digits after the dot. `decimals == 0` omits the dot
/// entirely. Negative inputs get a leading "-".
fn format_fixed_decimal(n : Int64, decimals : Int) -> String {
let buf = StringBuilder::new()
buf.write_string("\{n}")
if decimals > 0 {
buf.write_char('.')
for _ in 0.. Int64 {
let mut result = 1L
for _ in 0.. Unit {
let raw = value.to_string()
let mut i = raw.length()
while i < width {
buf.write_char('0')
i = i + 1
}
buf.write_string(raw)
}
///|
fn is_negative_zero(d : Double) -> Bool {
d.reinterpret_as_uint64() == 0x8000000000000000UL
}
///|
fn negate_float(d : Double) -> Double {
if d == 0.0 {
0x8000000000000000UL.reinterpret_as_double()
} else {
0.0 - d
}
}
///|
fn decimal_digit_at(text : String, index : Int) -> Int {
text.unsafe_get(index).to_int() - '0'.to_int()
}
///|
fn increment_decimal_string(text : String) -> String {
if text.length() == 0 {
return "1"
}
let digits : Array[Int] = []
for i in 0..= 0 && carry > 0 {
let next = digits[i] + carry
if next >= 10 {
digits[i] = 0
carry = 1
} else {
digits[i] = next
carry = 0
}
i = i - 1
}
let buf = StringBuilder::new()
if carry > 0 {
buf.write_char('1')
}
for digit in digits {
buf.write_char("0123456789".unsafe_get(digit).to_int().unsafe_to_char())
}
buf.to_string()
}
///|
fn split_decimal_text(text : String) -> (String, String) {
match text.find(".") {
Some(index) =>
(
String::unsafe_substring(text, start=0, end=index),
String::unsafe_substring(text, start=index + 1, end=text.length()),
)
None => (text, "")
}
}
///|
fn format_fixed_decimal_text(
whole_text : String,
fraction_text : String,
decimals : Int,
) -> String {
let digits : Array[Int] = []
for i in 0..= 5 {
let mut carry = 1
let mut i = digits.length() - 1
while i >= 0 && carry > 0 {
let next = digits[i] + carry
if next >= 10 {
digits[i] = 0
carry = 1
} else {
digits[i] = next
carry = 0
}
i = i - 1
}
if carry > 0 {
whole = increment_decimal_string(whole)
}
}
let buf = StringBuilder::new()
buf.write_string(whole)
if decimals > 0 {
buf.write_char('.')
for digit in digits {
buf.write_char("0123456789".unsafe_get(digit).to_int().unsafe_to_char())
}
}
buf.to_string()
}
///|
fn format_fixed_float_slow(
d : Double,
decimals : Int,
negative : Bool,
) -> String {
let scale = @math.pow(10.0, decimals.to_double())
let abs_value = if negative { 0.0 - d } else { d }
let rounded = (abs_value * scale + 0.5).floor() / scale
let whole = rounded.floor()
let buf = StringBuilder::new()
if negative {
buf.write_char('-')
}
buf.write_string(whole.to_int64().to_string())
if decimals > 0 {
buf.write_char('.')
let mut fraction = rounded - whole
for _ in 0.. String {
if d.is_nan() || d.is_inf() {
return render_float_text(d)
}
let negative = d < 0.0 || is_negative_zero(d)
let abs_value = if negative { 0.0 - d } else { d }
// `toFixed` works from the plain decimal expansion when the runtime
// provides one. `render_float_text` intentionally switches large values
// to Apple-style scientific PCF, which would route otherwise-fixable
// values like 123456789.12345679 through the less precise scaled path.
let text = abs_value.to_string()
if text.find("e") is None && text.find("E") is None {
let (whole, fraction) = split_decimal_text(text)
let fixed = format_fixed_decimal_text(whole, fraction, decimals)
return if negative { "-\{fixed}" } else { fixed }
}
if decimals > 18 {
return format_fixed_float_slow(d, decimals, negative)
}
let scale = pow10_int64(decimals)
if abs_value > 9_000_000_000_000_000_000.0 / scale.to_double() {
return format_fixed_float_slow(d, decimals, negative)
}
let scaled = (abs_value * scale.to_double() + 0.5).floor().to_int64()
let whole = scaled / scale
let fraction = scaled % scale
let buf = StringBuilder::new()
if negative {
buf.write_char('-')
}
buf.write_string(whole.to_string())
if decimals > 0 {
buf.write_char('.')
write_left_padded_int64(buf, fraction, decimals)
}
buf.to_string()
}
///|
fn render_float_to_int_error_text(d : Double) -> String {
let text = render_float_text(d)
match text {
"9223372036854778000.0" => "9.223372036854776E18"
"-9223372036854778000.0" => "-9.223372036854776E18"
_ => text
}
}
///|
/// PKL-150: format an Int with Apple Pkl-style "," thousand-grouping
/// (e.g. `1,114,112`). Negative sign is preserved.
fn format_int_with_commas(n : Int64) -> String {
let raw_signed = "\{n}"
let negative = raw_signed.has_prefix("-")
let raw = if negative {
String::unsafe_substring(raw_signed, start=1, end=raw_signed.length())
} else {
raw_signed
}
if raw.length() <= 3 {
return if negative { "-\{raw}" } else { raw }
}
let buf = StringBuilder::new()
let len = raw.length()
let first_group = len % 3
let mut i = 0
if first_group > 0 {
buf.write_string(String::unsafe_substring(raw, start=0, end=first_group))
i = first_group
}
while i < len {
if buf.to_string().length() > 0 {
buf.write_char(',')
}
buf.write_string(String::unsafe_substring(raw, start=i, end=i + 3))
i = i + 3
}
if negative {
"-" + buf.to_string()
} else {
buf.to_string()
}
}
///|
/// PKL-148: pkl:base Bool method surface. Apple Pkl projects logical
/// operators (`xor` / `implies`) and the keyword-named pair (`and` /
/// `or`) as instance methods; `toString` is universal. The methods
/// take a single Boolean argument except `toString` (arity 0) and the
/// unary `not` (arity 0).
fn eval_bool_method(
b : Bool,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"toString" =>
if arg_values.length() == 0 {
if b {
Some(StringValue("true"))
} else {
Some(StringValue("false"))
}
} else {
diagnostics.push(diag("Bool.toString takes no arguments"))
None
}
"xor" | "implies" | "and" | "or" =>
if arg_values.length() == 1 {
match arg_values[0] {
BoolValue(other) =>
match method_name {
"xor" => Some(BoolValue(b != other))
"implies" => Some(BoolValue(!b || other))
"and" => Some(BoolValue(b && other))
"or" => Some(BoolValue(b || other))
_ => None
}
other => {
diagnostics.push(
diag(
"Expected value of type `Boolean`, but got type `\{eval_value_type_name(other)}`. Value: \{render_pcf_value_inline(other)}",
),
)
None
}
}
} else {
diagnostics.push(
diag("Bool.\{method_name} expects exactly one argument"),
)
None
}
_ => {
diagnostics.push(
diag("Cannot find method `\{method_name}` in object of type `Boolean`."),
)
None
}
}
}
///|
/// PKL-148: pkl:base Float method surface. Most Float operations
/// share the Int name surface; the runtime intercepts a small set
/// of float-specific names (`isNaN`, `isFinite`, `isInfinite`).
fn eval_float_method(
d : Double,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"toString" =>
if arg_values.length() == 0 {
Some(StringValue(render_float_text(d)))
} else {
diagnostics.push(diag("Float.toString takes no arguments"))
None
}
"toFloat" =>
if arg_values.length() == 0 {
Some(FloatValue(d))
} else {
diagnostics.push(diag("Float.toFloat takes no arguments"))
None
}
"toInt" =>
if arg_values.length() == 0 {
if d.is_nan() || d.is_inf() {
diagnostics.push(
diag(
"Cannot convert non-finite Float `\{render_float_text(d)}` to Int.",
),
)
None
} else if d >= 9_223_372_036_854_776_000.0 ||
d < -9_223_372_036_854_776_000.0 {
diagnostics.push(
diag(
"Cannot convert Float `\{render_float_to_int_error_text(d)}` to Int because it is too large.",
),
)
None
} else {
Some(IntValue(d.to_int64()))
}
} else {
diagnostics.push(diag("Float.toInt takes no arguments"))
None
}
"toFixed" => {
if arg_values.length() != 1 {
diagnostics.push(
diag("Float.toFixed expects 1 argument, got \{arg_values.length()}"),
)
return None
}
match arg_values[0] {
IntValue(decimals) =>
if decimals < 0L || decimals > 20L {
diagnostics.push(
diag(
"Type constraint `this.isBetween(0, 20)` violated. Value: \{decimals}",
),
)
None
} else {
Some(StringValue(format_fixed_float(d, decimals.to_int())))
}
_ => {
diagnostics.push(diag("Float.toFixed expects Int argument"))
None
}
}
}
"isBetween" => {
if arg_values.length() != 2 {
diagnostics.push(
diag(
"Float.isBetween expects 2 arguments, got \{arg_values.length()}",
),
)
return None
}
let lower = match arg_values[0] {
IntValue(v) => v.to_double()
FloatValue(v) => v
_ => {
diagnostics.push(diag("Float.isBetween expects Number arguments"))
return None
}
}
let upper = match arg_values[1] {
IntValue(v) => v.to_double()
FloatValue(v) => v
_ => {
diagnostics.push(diag("Float.isBetween expects Number arguments"))
return None
}
}
Some(BoolValue(d >= lower && d <= upper))
}
"toDuration" => {
if arg_values.length() != 1 {
diagnostics.push(
diag(
"Float.toDuration expects 1 argument, got \{arg_values.length()}",
),
)
return None
}
match arg_values[0] {
StringValue(unit) =>
if is_duration_unit_name(unit) {
Some(DurationValue(d, unit))
} else {
diagnostics.push(diag("Invalid Duration unit: \"\{unit}\"."))
None
}
_ => {
diagnostics.push(diag("Float.toDuration expects String argument"))
None
}
}
}
"toDataSize" => {
if arg_values.length() != 1 {
diagnostics.push(
diag(
"Float.toDataSize expects 1 argument, got \{arg_values.length()}",
),
)
return None
}
match arg_values[0] {
StringValue(unit) =>
if is_datasize_unit_name(unit) {
Some(DataSizeValue(d, unit))
} else {
diagnostics.push(diag("Invalid DataSize unit: \"\{unit}\"."))
None
}
_ => {
diagnostics.push(diag("Float.toDataSize expects String argument"))
None
}
}
}
"abs" =>
if arg_values.length() == 0 {
Some(FloatValue(if d < 0.0 { -d } else { d }))
} else {
diagnostics.push(diag("Float.abs takes no arguments"))
None
}
"round" =>
if arg_values.length() == 0 {
// Apple Pkl keeps Float identity for Float.round().
if is_negative_zero(d) {
Some(FloatValue(d))
} else {
Some(FloatValue(d.round()))
}
} else {
diagnostics.push(diag("Float.round takes no arguments"))
None
}
"floor" =>
if arg_values.length() == 0 {
Some(FloatValue(d.floor()))
} else {
diagnostics.push(diag("Float.floor takes no arguments"))
None
}
"ceil" =>
if arg_values.length() == 0 {
Some(FloatValue(d.ceil()))
} else {
diagnostics.push(diag("Float.ceil takes no arguments"))
None
}
"truncate" =>
if arg_values.length() == 0 {
if d.is_nan() || d.is_inf() {
Some(FloatValue(d))
} else if d < 0.0 {
Some(FloatValue(d.ceil()))
} else if is_negative_zero(d) {
Some(FloatValue(d))
} else {
Some(FloatValue(d.floor()))
}
} else {
diagnostics.push(diag("Float.truncate takes no arguments"))
None
}
"isPositive" | "isNonZero" | "isFinite" | "isNaN" | "isInfinite" | "sign" =>
if arg_values.length() != 0 {
diagnostics.push(
diag(
"Float.\{method_name} expects 0 arguments, got \{arg_values.length()}",
),
)
None
} else {
eval_float_property(d, method_name, diagnostics)
}
_ => {
diagnostics.push(
diag("Cannot find method `\{method_name}` in object of type `Float`."),
)
None
}
}
}
///|
fn eval_duration_method(
magnitude : Double,
unit : String,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"isBetween" => {
if arg_values.length() != 2 {
diagnostics.push(
diag(
"Duration.isBetween expects 2 arguments, got \{arg_values.length()}",
),
)
return None
}
match (arg_values[0], arg_values[1]) {
(DurationValue(lower, lower_unit), DurationValue(upper, upper_unit)) => {
let value_in_ns = duration_in_unit(magnitude, unit, "ns")
let lower_in_ns = duration_in_unit(lower, lower_unit, "ns")
let upper_in_ns = duration_in_unit(upper, upper_unit, "ns")
Some(
BoolValue(value_in_ns >= lower_in_ns && value_in_ns <= upper_in_ns),
)
}
_ => {
diagnostics.push(
diag("Duration.isBetween expects Duration arguments"),
)
None
}
}
}
"toUnit" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(target) =>
if is_duration_unit_name(target) {
Some(
DurationValue(duration_in_unit(magnitude, unit, target), target),
)
} else {
diagnostics.push(
diag(
"Expected value of type `\"ns\"|\"us\"|\"ms\"|\"s\"|\"min\"|\"h\"|\"d\"`, but got `\"\{target}\"`.",
),
)
None
}
_ => {
diagnostics.push(diag("Duration.toUnit expects a String argument"))
None
}
}
} else {
diagnostics.push(diag("Duration.toUnit expects exactly one argument"))
None
}
_ => {
diagnostics.push(
diag(
"Cannot find method `\{method_name}` in object of type `Duration`.",
),
)
None
}
}
}
///|
fn eval_datasize_method(
magnitude : Double,
unit : String,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
match method_name {
"isBetween" => {
if arg_values.length() != 2 {
diagnostics.push(
diag(
"DataSize.isBetween expects 2 arguments, got \{arg_values.length()}",
),
)
return None
}
match (arg_values[0], arg_values[1]) {
(DataSizeValue(lower, lower_unit), DataSizeValue(upper, upper_unit)) => {
let value_in_bytes = datasize_in_unit(magnitude, unit, "b")
let lower_in_bytes = datasize_in_unit(lower, lower_unit, "b")
let upper_in_bytes = datasize_in_unit(upper, upper_unit, "b")
Some(
BoolValue(
value_in_bytes >= lower_in_bytes &&
value_in_bytes <= upper_in_bytes,
),
)
}
_ => {
diagnostics.push(
diag("DataSize.isBetween expects DataSize arguments"),
)
None
}
}
}
"toUnit" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(target) =>
if is_datasize_unit_name(target) {
Some(
DataSizeValue(datasize_in_unit(magnitude, unit, target), target),
)
} else {
diagnostics.push(
diag(
"Expected value of type `\"b\"|\"kb\"|\"kib\"|\"mb\"|\"mib\"|\"gb\"|\"gib\"|\"tb\"|\"tib\"|\"pb\"|\"pib\"`, but got `\"\{target}\"`.",
),
)
None
}
_ => {
diagnostics.push(diag("DataSize.toUnit expects a String argument"))
None
}
}
} else {
diagnostics.push(diag("DataSize.toUnit expects exactly one argument"))
None
}
"toBinaryUnit" | "toDecimalUnit" =>
if arg_values.length() == 0 {
let target = if method_name == "toBinaryUnit" {
datasize_binary_unit_for(unit)
} else {
datasize_decimal_unit_for(unit)
}
Some(DataSizeValue(datasize_in_unit(magnitude, unit, target), target))
} else {
diagnostics.push(diag("DataSize.\{method_name} takes no arguments"))
None
}
_ => {
diagnostics.push(
diag(
"Cannot find method `\{method_name}` in object of type `DataSize`.",
),
)
None
}
}
}
///|
fn eval_regex_property(
pattern : String,
name : String,
diagnostics : Array[Diagnostic],
) -> Value? {
match name {
"pattern" => Some(StringValue(pattern))
"groupCount" =>
Some(IntValue(regex_capture_group_count(pattern).to_int64()))
_ => {
diagnostics.push(
diag("Cannot find property `\{name}` in object of type `Regex`."),
)
None
}
}
}
///|
fn eval_regex_method(
pattern : String,
method_name : String,
arguments : Array[Expr],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let arg_values : Array[Value] = []
let mut ok = true
for argument in arguments {
match
eval_expr_with_bindings(
argument, bindings, env, class_env, cache, stack, declarations, diagnostics,
resolve_import,
) {
Some(v) => arg_values.push(v)
None => ok = false
}
}
if !ok {
return None
}
let re = match compile_regex_pattern(pattern, diagnostics) {
Some(r) => r
None => return None
}
match method_name {
"matches" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(input) => Some(BoolValue(regex_matches_full(re, input)))
_ => {
diagnostics.push(diag("Regex.matches expects a String argument"))
None
}
}
} else {
diagnostics.push(diag("Regex.matches expects exactly one argument"))
None
}
"find" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(input) =>
match regex_find_first(re, input) {
Some(found) => Some(StringValue(found))
None => Some(NullValue)
}
_ => {
diagnostics.push(diag("Regex.find expects a String argument"))
None
}
}
} else {
diagnostics.push(diag("Regex.find expects exactly one argument"))
None
}
"findAll" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(input) => {
let matches = regex_find_all(re, input)
let elements : Array[Value] = []
for s in matches {
elements.push(StringValue(s))
}
Some(ListingValue(elements))
}
_ => {
diagnostics.push(diag("Regex.findAll expects a String argument"))
None
}
}
} else {
diagnostics.push(diag("Regex.findAll expects exactly one argument"))
None
}
"findMatchesIn" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(input) =>
Some(ListValue(regex_find_match_objects(re, input)))
_ => {
diagnostics.push(
diag("Regex.findMatchesIn expects a String argument"),
)
None
}
}
} else {
diagnostics.push(
diag("Regex.findMatchesIn expects exactly one argument"),
)
None
}
"matchEntire" =>
if arg_values.length() == 1 {
match arg_values[0] {
StringValue(input) =>
match re.match_(input) {
Some(match_result) =>
if match_result.before().length() == 0 &&
match_result.after().length() == 0 {
Some(regex_match_object(re, match_result, 0))
} else {
Some(NullValue)
}
None => Some(NullValue)
}
_ => {
diagnostics.push(
diag("Regex.matchEntire expects a String argument"),
)
None
}
}
} else {
diagnostics.push(diag("Regex.matchEntire expects exactly one argument"))
None
}
"replace" =>
if arg_values.length() == 2 {
match (arg_values[0], arg_values[1]) {
(StringValue(input), StringValue(repl)) =>
Some(StringValue(regex_replace_first(re, input, repl)))
_ => {
diagnostics.push(
diag("Regex.replace expects (String, String) arguments"),
)
None
}
}
} else {
diagnostics.push(diag("Regex.replace expects exactly two arguments"))
None
}
"replaceAll" =>
if arg_values.length() == 2 {
match (arg_values[0], arg_values[1]) {
(StringValue(input), StringValue(repl)) =>
Some(StringValue(regex_replace_all(re, input, repl)))
_ => {
diagnostics.push(
diag("Regex.replaceAll expects (String, String) arguments"),
)
None
}
}
} else {
diagnostics.push(diag("Regex.replaceAll expects exactly two arguments"))
None
}
_ => {
diagnostics.push(
diag("Cannot find method `\{method_name}` in object of type `Regex`."),
)
None
}
}
}