///|
pub(all) struct ConstructorType {
name : String
inputs : Array[ValueType]
}
///|
pub(all) struct CustomType {
name : String
constructors : Array[ConstructorType]
}
///|
pub fn ValueType::name(self : ValueType) -> String {
match self {
Int => "int"
Bool => "bool"
Unit => "unit"
Decimal => "decimal"
Float64 => "float64"
Text => "text"
Character => "character"
Sequence(_) => "list"
Product(_) => "tuple"
Record(_) => "record"
Custom(_) => "custom"
Optional(_) => "maybe"
Outcome(_, _) => "result"
Function => "function"
}
}
///|
fn integer_string(source : String) -> @bigint.BigInt raise {
let chars = source.to_array()
let start = if source.has_prefix("-") { 1 } else { 0 }
if chars.length() <= start ||
chars.length() > 10000 ||
!chars[start:].iter().all(c => c >= '0' && c <= '9') {
raise Invalid("execution.invalid_integer")
}
@bigint.BigInt::from_string(source)
}
///|
fn small_integer(value : @ion.IonValue) -> Int32 raise {
if value.has_annotations() {
raise Invalid("execution.unexpected_annotation")
}
match value.kind() {
Int(n) => {
if n.compare_int(-2147483648) < 0 || n.compare_int(2147483647) > 0 {
raise Invalid("execution.integer_range")
}
n.to_int()
}
_ => raise Invalid("execution.expected_integer")
}
}
///|
pub fn normalize_decimal(
coefficient : @bigint.BigInt,
exponent : Int32,
) -> (@bigint.BigInt, Int32) raise {
if coefficient.to_string().length() > 10000 ||
exponent < -10000 ||
exponent > 10000 {
raise Invalid("execution.decimal_limit")
}
if coefficient.is_zero() {
return (coefficient, 0)
}
let ten = @bigint.BigInt::from_int(10)
let mut n = coefficient
let mut e = exponent
while n / ten * ten == n {
n = n / ten
e += 1
}
if e < -32 || e > 10000 {
raise Invalid("execution.decimal_precision")
}
(n, e)
}
///|
/// Scientific form accepted by the concrete SDK without loss of precision.
pub fn decimal_source(
coefficient : @bigint.BigInt,
exponent : Int32,
) -> String raise {
let (n, e) = normalize_decimal(coefficient, exponent)
if n.is_zero() {
return "0"
}
let source = n.to_string()
let negative = source.has_prefix("-")
let digits = if negative { source[1:].to_owned() } else { source }
(if negative { "-" } else { "" }) +
digits[0:1].to_owned() +
(if digits.length() > 1 { "." + digits[1:].to_owned() } else { "" }) +
"e" +
(e + digits.length() - 1).to_string()
}
///|
fn valid_model_error(code : String) -> Bool {
[
"sdk.division_by_zero", "sdk.invalid_number", "sdk.out_of_range", "sdk.invalid_comparison",
"sdk.unrepresentable_text", "sdk.decimal.division_by_zero", "sdk.decimal.underflow",
"sdk.decimal.precision_loss", "sdk.decimal.invalid_literal",
].contains(code)
}
///|
/// A shared aggregate budget across separately returned portable values. Failed
/// acceptance leaves the previous budget unchanged.
pub struct ValueBudget {
priv remaining : Int32
}
///|
pub fn ValueBudget::new() -> ValueBudget {
{ remaining: 100000, }
}
///|
pub fn ValueBudget::accept(
self : ValueBudget,
value : Value,
) -> ValueBudget raise {
let remaining = Ref(self.remaining)
validate_value(value, 0, remaining)
{ remaining: remaining.val, }
}
///|
fn validate_value(
value : Value,
depth : Int32,
budget : Ref[Int32],
) -> Unit raise {
budget.val -= 1
if depth > 64 || budget.val < 0 {
raise Invalid("execution.value_limit")
}
fn child(v : Value) -> Unit raise {
validate_value(v, depth + 1, budget)
}
match value {
Integer(n) =>
if n.to_string().length() > 10000 {
raise Invalid("execution.integer_limit")
}
Decimal(n, e) => ignore(normalize_decimal(n, e))
Text(units) | Character(units) => {
budget.val -= units.length()
if units.any(n => n < 0 || n > 65535) || budget.val < 0 {
raise Invalid("execution.utf16_units")
}
}
Sequence(items) | Product(items) =>
for item in items {
child(item)
}
Record(items) => {
let seen = Map([])
for (name, v) in items {
if name == "" || name.length() > 1024 || seen.contains(name) {
raise Invalid("execution.record_field: " + name)
}
seen[name] = true
child(v)
}
}
Constructor(owner, tag, items) => {
if owner == "" ||
tag == "" ||
owner.length() > 1024 ||
tag.length() > 1024 {
raise Invalid("execution.constructor_identity")
}
for item in items {
child(item)
}
}
Optional(Some(v)) | Outcome(Ok(v)) | Outcome(Err(v)) => child(v)
ModelError(code) =>
if !valid_model_error(code) {
raise Invalid("execution.model_error_code")
}
_ => ()
}
}
///|
/// A declared type is authoritative, including empty containers and recursive ADTs.
pub fn validate_as(
value : Value,
expected : ValueType,
definitions : Array[CustomType],
output~ : Bool = false,
) -> Unit raise {
validate_value(value, 0, Ref(100000))
if value is ModelError(_) {
if output {
return
}
raise Invalid("execution.model_error_argument")
}
validate_typed(value, expected, definitions, "value", 0)
}
///|
fn validate_typed(
value : Value,
expected : ValueType,
definitions : Array[CustomType],
path : String,
depth : Int32,
) -> Unit raise {
if depth > 64 {
raise Invalid("execution.value_limit: " + path)
}
fn child(v : Value, t : ValueType, name : String) -> Unit raise {
validate_typed(v, t, definitions, path + "." + name, depth + 1)
}
match (value, expected) {
(Integer(_), Int)
| (Boolean(_), Bool)
| (Unit, Unit)
| (Decimal(_, _), Decimal)
| (Float64(_), Float64)
| (Text(_), Text)
| (Character(_), Character) => ()
(Sequence(items), Sequence(t)) =>
for i, item in items {
child(item, t, i.to_string())
}
(Product(items), Product(types)) => {
if items.length() != types.length() {
raise Invalid("execution.tuple_arity: " + path)
}
for i, item in items {
child(item, types[i], i.to_string())
}
}
(Record(items), Record(types)) => {
let fields = Map(items)
for (name, t) in types {
guard fields.get(name) is Some(v) else {
raise Invalid("execution.missing_record_field: " + path + "." + name)
}
child(v, t, name)
}
for (name, _) in items {
if !types.any(p => p.0 == name) {
raise Invalid("execution.extra_record_field: " + path + "." + name)
}
}
}
(Constructor(owner, tag, items), Custom(name)) => {
if owner != name {
raise Invalid("execution.constructor_owner: " + path)
}
guard definitions.filter(d => d.name == name).get(0) is Some(definition) else {
raise Invalid("execution.unresolved_custom_type: " + name)
}
guard definition.constructors.filter(c => c.name == tag).get(0)
is Some(variant) else {
raise Invalid("execution.constructor_tag: " + path)
}
if variant.inputs.length() != items.length() {
raise Invalid("execution.constructor_arity: " + path)
}
for i, item in items {
child(item, variant.inputs[i], i.to_string())
}
}
(Optional(None), Optional(_)) => ()
(Optional(Some(v)), Optional(t)) => child(v, t, "just")
(Outcome(Ok(v)), Outcome(ok, _)) => child(v, ok, "ok")
(Outcome(Err(v)), Outcome(_, err)) => child(v, err, "err")
(_, Function) => raise Invalid("execution.boundary_function: " + path)
_ =>
raise Invalid(
"execution.wrong_type: " + path + ": expected " + expected.name(),
)
}
}
///|
pub fn Value::to_json(self : Value) -> Json {
match self {
Integer(n) => { "type": "int", "value": n.to_string() }
Boolean(b) => { "type": "bool", "value": b }
Unit => { "type": "unit" }
Decimal(n, e) =>
{ "type": "decimal", "coefficient": n.to_string(), "exponent": e }
Float64(bits) => { "type": "float64", "bits": bits.to_string() }
Text(units) => { "type": "text", "units": units }
Character(units) => { "type": "character", "units": units }
Sequence(items) => { "type": "list", "items": items.map(Value::to_json) }
Product(items) => { "type": "tuple", "items": items.map(Value::to_json) }
Record(items) =>
{
"type": "record",
"fields": items.map(p => {
({ "name": p.0, "value": p.1.to_json() } : Json)
}),
}
Constructor(owner, tag, items) =>
{
"type": "custom",
"owner": owner,
"tag": tag,
"items": items.map(Value::to_json),
}
Optional(None) => { "type": "maybe", "case": "nothing" }
Optional(Some(v)) =>
{ "type": "maybe", "case": "just", "value": v.to_json() }
Outcome(Ok(v)) => { "type": "result", "case": "ok", "value": v.to_json() }
Outcome(Err(v)) => { "type": "result", "case": "err", "value": v.to_json() }
ModelError(code) => { "type": "model-error", "code": code }
}
}
///|
fn payload_value(value : @ion.IonValue, depth : Int32) -> Value raise {
if depth > 64 {
raise Invalid("execution.value_limit")
}
if value.has_annotations() {
raise Invalid("execution.unexpected_annotation")
}
let header = fields(value, [
"type", "value", "coefficient", "exponent", "bits", "units", "items", "fields",
"owner", "tag", "case", "code",
])
let kind = string(required(header, "type"))
let allowed = match kind {
"unit" => ["type"]
"int" | "bool" => ["type", "value"]
"decimal" => ["type", "coefficient", "exponent"]
"float64" => ["type", "bits"]
"text" | "character" => ["type", "units"]
"list" | "tuple" => ["type", "items"]
"record" => ["type", "fields"]
"custom" => ["type", "owner", "tag", "items"]
"maybe" =>
if string(required(header, "case")) == "nothing" {
["type", "case"]
} else {
["type", "case", "value"]
}
"result" => ["type", "case", "value"]
"model-error" => ["type", "code"]
_ => raise Invalid("execution.unsupported_value")
}
let f = fields(value, allowed)
if f.length() != allowed.length() {
raise Invalid("execution.invalid_json_value")
}
fn child(v : @ion.IonValue) -> Value raise {
payload_value(v, depth + 1)
}
let result = match kind {
"int" => Integer(integer_string(string(required(f, "value"))))
"bool" =>
match required(f, "value").kind() {
Bool(b) => {
if required(f, "value").has_annotations() {
raise Invalid("execution.unexpected_annotation")
}
Boolean(b)
}
_ => raise Invalid("execution.expected_bool")
}
"unit" => Unit
"decimal" =>
Decimal(
integer_string(string(required(f, "coefficient"))),
small_integer(required(f, "exponent")),
)
"float64" => {
let bits = integer_string(string(required(f, "bits")))
if bits.compare_int(0) < 0 ||
bits.compare_uint64(18446744073709551615UL) > 0 {
raise Invalid("execution.float_bits")
}
Float64(bits.to_uint64())
}
"text" | "character" => {
let units = list(required(f, "units")).map(small_integer)
if kind == "text" {
Text(units)
} else {
Character(units)
}
}
"list" | "tuple" => {
let items = list(required(f, "items")).map(child)
if kind == "list" {
Sequence(items)
} else {
Product(items)
}
}
"record" =>
Record(
list(required(f, "fields")).map(v => {
if v.has_annotations() {
raise Invalid("execution.unexpected_annotation")
}
let f = fields(v, ["name", "value"])
(string(required(f, "name")), child(required(f, "value")))
}),
)
"custom" =>
Constructor(
string(required(f, "owner")),
string(required(f, "tag")),
list(required(f, "items")).map(child),
)
"maybe" =>
match string(required(f, "case")) {
"nothing" => Optional(None)
"just" => Optional(Some(child(required(f, "value"))))
_ => raise Invalid("execution.maybe_case")
}
"result" =>
match string(required(f, "case")) {
"ok" => Outcome(Ok(child(required(f, "value"))))
"err" => Outcome(Err(child(required(f, "value"))))
_ => raise Invalid("execution.result_case")
}
"model-error" => ModelError(string(required(f, "code")))
_ => raise Invalid("execution.unsupported_value")
}
result
}
///|
pub fn Value::from_json(value : Json) -> Value raise {
let result = payload_value(
@text.read_ion(value.stringify()[:], fuel=Limited(@ion.Budget(128))),
0,
)
validate_value(result, 0, Ref(100000))
result
}
///|
pub fn Value::to_ion(self : Value) -> @ion.IonValue {
match self {
Integer(n) => @ion.Ion(@ion.IonValueKind::Int(n))
Boolean(b) => @ion.IonValue::bool(b)
Unit =>
@ion.IonValue::untyped_null().with_annotation(@ion.symbol("morphir_unit"))
Decimal(n, e) => @ion.IonValue::decimal(@ion.decimal(n, e))
_ => value_payload(self).with_annotation(@ion.symbol("morphir_value"))
}
}
///|
pub fn Value::from_ion(value : @ion.IonValue) -> Value raise {
let annotations = value.annotations()
let result = if annotations.is_empty() {
match value.kind() {
Int(n) => Integer(n)
Bool(b) => Boolean(b)
Decimal(d) => Decimal(d.coefficient(), d.exponent())
_ => raise Invalid("execution.unsupported_value")
}
} else if annotations.length() == 1 &&
annotations[0].text() == Some("morphir_unit") &&
value.kind() is Null(@ion.Null) {
Unit
} else if annotations.length() == 1 &&
annotations[0].text() == Some("morphir_value") {
payload_value(@ion.Ion(value.kind()), 0)
} else {
raise Invalid("execution.value_annotation")
}
validate_value(result, 0, Ref(100000))
result
}
///|
fn value_payload(value : Value) -> @ion.IonValue {
fn text(s : String) -> @ion.IonValue {
@ion.IonValue::string(s)
}
fn items(vs : Array[Value]) -> @ion.IonValue {
@ion.IonValue::list(vs.map(value_payload))
}
let pairs = match value {
Integer(n) => [("type", text("int")), ("value", text(n.to_string()))]
Boolean(b) => [("type", text("bool")), ("value", @ion.IonValue::bool(b))]
Unit => [("type", text("unit"))]
Decimal(n, e) =>
[
("type", text("decimal")),
("coefficient", text(n.to_string())),
("exponent", @ion.IonValue::int(e)),
]
Float64(bits) =>
[("type", text("float64")), ("bits", text(bits.to_string()))]
Text(units) | Character(units) =>
[
("type", text(if value is Text(_) { "text" } else { "character" })),
("units", @ion.IonValue::list(units.map(@ion.IonValue::int))),
]
Sequence(vs) | Product(vs) =>
[
("type", text(if value is Sequence(_) { "list" } else { "tuple" })),
("items", items(vs)),
]
Record(fields) =>
[
("type", text("record")),
(
"fields",
@ion.IonValue::list(
fields.map(p => {
record([("name", text(p.0)), ("value", value_payload(p.1))])
}),
),
),
]
Constructor(owner, tag, vs) =>
[
("type", text("custom")),
("owner", text(owner)),
("tag", text(tag)),
("items", items(vs)),
]
Optional(None) => [("type", text("maybe")), ("case", text("nothing"))]
Optional(Some(v)) =>
[
("type", text("maybe")),
("case", text("just")),
("value", value_payload(v)),
]
Outcome(Ok(v)) | Outcome(Err(v)) =>
[
("type", text("result")),
("case", text(if value is Outcome(Ok(_)) { "ok" } else { "err" })),
("value", value_payload(v)),
]
ModelError(code) => [("type", text("model-error")), ("code", text(code))]
}
record(pairs)
}
///|
pub fn ValueType::to_json(self : ValueType) -> Json {
match self {
Sequence(t) | Optional(t) => { "type": self.name(), "item": t.to_json() }
Product(types) =>
{ "type": "tuple", "items": types.map(ValueType::to_json) }
Record(fields) =>
{
"type": "record",
"fields": fields.map(p => {
({ "name": p.0, "type": p.1.to_json() } : Json)
}),
}
Custom(name) => { "type": "custom", "name": name }
Outcome(ok, err) =>
{ "type": "result", "ok": ok.to_json(), "err": err.to_json() }
_ => { "type": self.name() }
}
}