///|
/// Parser-owned nodes remain inside this adapter until declarations are admitted.
priv struct ParsedLibrary {
nodes : Array[@ast.Impl]
source_chars : Array[Char]
}
///|
fn library_parse(input : Input, cancelled : () -> Bool) -> ParsedLibrary raise {
check_lexing(input)
let (nodes, reports) = @parser.parse_string(input.source, name=input.unit_id)
library_poll(cancelled)
if !reports.is_empty() {
reject("syntax", reports[0].msg, reports[0].loc)
}
{ nodes: nodes.to_array(), source_chars: input.source.to_array(), }
}
///|
priv struct LibraryAdapter {
source_chars : Array[Char]
channel : SourceChannel
signatures : Map[String, Array[LibraryTypeUse]]
limits : LibraryLimits
cancelled : () -> Bool
mut expressions : Int32
mut calls : Int32
mut lets : Int32
}
///|
fn LibraryAdapter::visit(
self : LibraryAdapter,
loc : @location.Location,
depth : Int32,
) -> Unit raise {
library_poll(self.cancelled)
if depth > self.limits.max_depth ||
self.expressions >= self.limits.max_expressions {
reject("expression_limit", "Expression traversal budget exceeded", loc)
}
self.expressions += 1
}
///|
fn LibraryAdapter::number(
self : LibraryAdapter,
raw : String,
scalar : LibraryScalar,
loc : @location.Location,
) -> LibraryExpr raise {
let start = loc.start.cnum
let end = loc.end.cnum
if start < 0 || end < start || end > self.source_chars.length() {
reject("source_coordinate", "Invalid numeric literal coordinate", loc)
}
let authored = String::from_array(self.source_chars[start:end].to_owned())
Number(library_integer_literal(raw, scalar, loc), authored, scalar, span(loc))
}
///|
fn LibraryAdapter::floating(
self : LibraryAdapter,
raw : String,
scalar : LibraryScalar,
loc : @location.Location,
) -> LibraryExpr raise {
let start = loc.start.cnum
let end = loc.end.cnum
if start < 0 || end < start || end > self.source_chars.length() {
reject("source_coordinate", "Invalid floating literal coordinate", loc)
}
let authored = String::from_array(self.source_chars[start:end].to_owned())
let spelling = if raw.has_suffix("F") || raw.has_suffix("f") {
raw[:raw.length() - 1].to_owned()
} else {
raw
}
let parsed = @parse.parse_double(spelling.exact_view()) catch {
_ => {
reject("invalid_literal", "Invalid floating literal", loc)
0.0
}
}
if parsed.is_nan() || parsed.is_inf() {
reject(
"invalid_literal", "Nonfinite floating literals are unsupported", loc,
)
}
let value = if scalar == Floating32 {
Float::from_double(parsed).to_double()
} else {
parsed
}
if value.is_inf() {
reject("invalid_literal", "Floating literal exceeds source range", loc)
}
Floating(value, authored, scalar, span(loc))
}
///|
/// Unsuffixed integer literals follow the expected source width in the small
/// expression forms whose type is fixed by their surrounding annotation.
fn LibraryAdapter::expression_as(
self : LibraryAdapter,
expr : @ast.Expr,
depth : Int32,
scalar : LibraryScalar,
) -> LibraryExpr raise {
if !scalar.is_integer() || scalar == SignedInt32 {
return self.expression(expr, depth)
}
let loc = expr.loc()
match expr {
@ast.Constant(c=@ast.Int(raw), ..) => {
self.visit(loc, depth)
self.number(raw, scalar, loc)
}
@ast.Group(expr=inner, ..) => {
self.visit(loc, depth)
self.expression_as(inner, depth + 1, scalar)
}
@ast.Unary(op~, expr=inner, ..) if identifier(op) == "-" &&
scalar.is_signed() => {
self.visit(loc, depth)
match inner {
@ast.Constant(c=@ast.Int(raw), ..) => {
self.visit(inner.loc(), depth + 1)
self.number("-" + raw, scalar, loc)
}
_ =>
NumericNegation(
self.expression_as(inner, depth + 1, scalar),
span(loc),
)
}
}
@ast.Infix(op~, lhs~, rhs~, ..) if ["+", "-", "*"].contains(identifier(op)) => {
self.visit(loc, depth)
Binary(
identifier(op),
self.expression_as(lhs, depth + 1, scalar),
self.expression_as(rhs, depth + 1, scalar),
span(loc),
)
}
@ast.If(cond~, ifso~, ifnot=Some(other), ..) => {
self.visit(loc, depth)
Conditional(
self.expression(cond, depth + 1),
self.expression_as(ifso, depth + 1, scalar),
self.expression_as(other, depth + 1, scalar),
span(loc),
)
}
_ => self.expression(expr, depth)
}
}
///|
fn LibraryAdapter::expression(
self : LibraryAdapter,
expr : @ast.Expr,
depth : Int32,
) -> LibraryExpr raise {
let loc = expr.loc()
let source = span(loc)
self.visit(loc, depth)
match expr {
@ast.Constant(c=@ast.Bool(value), ..) => Boolean(value, source)
@ast.Constant(c=@ast.Int(raw), ..) => self.number(raw, SignedInt32, loc)
@ast.Constant(c=@ast.Int64(raw), ..) => self.number(raw, SignedInt64, loc)
@ast.Constant(c=@ast.UInt(raw), ..) => self.number(raw, UnsignedInt32, loc)
@ast.Constant(c=@ast.UInt64(raw), ..) =>
self.number(raw, UnsignedInt64, loc)
@ast.Constant(c=@ast.BigInt(raw), ..) =>
self.number(raw, UnboundedInteger, loc)
@ast.Constant(c=@ast.Float(raw), ..) => self.floating(raw, Floating32, loc)
@ast.Constant(c=@ast.Double(raw), ..) => self.floating(raw, Floating64, loc)
@ast.Constant(c=@ast.String(repr), ..) => {
// The pinned lexer counts Unicode scalar positions, not UTF-8 bytes or UTF-16 units.
let start = loc.start.cnum
let end = loc.end.cnum
if start < 0 || end < start || end > self.source_chars.length() {
reject("source_coordinate", "Invalid literal coordinate", loc)
}
let authored = String::from_array(self.source_chars[start:end].to_owned())
if authored != "\"" + repr + "\"" {
reject(
"source_coordinate", "Literal token does not match pinned lexer coordinates",
loc,
)
}
Text(library_cook_string(repr, loc), authored, source)
}
@ast.Ident(id~, ..) => Identifier(identifier(id), source)
@ast.Group(expr~, ..) => self.expression(expr, depth + 1)
@ast.Constraint(expr~, ty~, ..) => {
let target = library_type(Some(ty), ty.loc(), self.channel)
Constraint(
self.expression_as(expr, depth + 1, target.scalar),
target,
source,
)
}
@ast.Unary(op~, expr~, ..) => {
let operator = identifier(op)
if operator == "-" {
return NumericNegation(self.expression(expr, depth + 1), source)
}
if operator != "!" {
reject(
"unsupported_operator", "Only Boolean negation is supported", loc,
)
}
Negation(self.expression(expr, depth + 1), source)
}
@ast.Infix(op~, lhs~, rhs~, ..) => {
let op = identifier(op)
if !["&&", "||", "==", "!=", "+", "-", "*", "/", "<", "<=", ">", ">="].contains(
op,
) {
reject(
"unsupported_operator", "Operator is outside the supported library subset",
loc,
)
}
Binary(
op,
self.expression(lhs, depth + 1),
self.expression(rhs, depth + 1),
source,
)
}
@ast.If(cond~, ifso~, ifnot=Some(other), ..) =>
Conditional(
self.expression(cond, depth + 1),
self.expression(ifso, depth + 1),
self.expression(other, depth + 1),
source,
)
@ast.Let(pattern~, expr~, body~, ..) => {
self.lets += 1
if self.lets > self.limits.max_lets {
reject("let_limit", "Function binding budget exceeded", loc)
}
let mut annotation = None
let binder = match pattern {
@ast.Var(binder) => binder
@ast.Constraint(pat=@ast.Var(binder), ty~, ..) => {
annotation = Some(library_type(Some(ty), ty.loc(), self.channel))
binder
}
_ => {
reject(
"unsupported_binding", "Only a single immutable named Boolean binding is supported",
loc,
)
abort("unreachable")
}
}
Binding(
binder.name,
span(binder.loc),
match annotation {
Some(ty) =>
Constraint(
self.expression_as(expr, depth + 1, ty.scalar),
ty,
source,
)
None => self.expression(expr, depth + 1)
},
self.expression(body, depth + 1),
source,
)
}
@ast.DotApply(self=receiver, method_name~, args~, return_self=false, ..) =>
match (method_name.name, receiver) {
("length", value) if args.is_empty() =>
StringLength(self.expression(value, depth + 1), source)
(
"to_owned",
@ast.ArrayGetSlice(
array~,
start_index=Some(start),
end_index=Some(end),
..
),
) if args.is_empty() =>
StringSlice(
self.expression(array, depth + 1),
self.expression(start, depth + 1),
self.expression(end, depth + 1),
source,
)
(name, value) => {
// Resolution, not this adapter, decides whether the method is pinned.
self.calls += 1
if self.calls > self.limits.max_calls {
reject("call_limit", "Unit call-site budget exceeded", loc)
}
let receiver = self.expression(value, depth + 1)
let arguments = []
for arg in args {
if !(arg.kind is @ast.Positional) {
reject(
"unsupported_arguments", "Only positional method arguments are supported",
loc,
)
}
arguments.push(self.expression(arg.value, depth + 1))
}
MethodCall(name, receiver, arguments, source)
}
}
@ast.Apply(func~, args~, ..) => {
guard func is @ast.Ident(id~, ..) else {
reject(
"unsupported_expression", "Only direct same-file or audited SDK calls are supported",
loc,
)
abort("unreachable")
}
self.visit(func.loc(), depth + 1)
self.calls += 1
if self.calls > self.limits.max_calls {
reject("call_limit", "Unit call-site budget exceeded", loc)
}
let (name, qualified) = match id.name {
@ast.Ident(name~) => (name, None)
@ast.Dot(pkg~, id~) => ("@" + pkg + "." + id, Some((pkg, id)))
}
let arguments = []
let mut i = 0
for arg in args {
if !(arg.kind is @ast.Positional) {
reject(
"unsupported_arguments", "Only positional call arguments are supported",
loc,
)
}
arguments.push(
match self.signatures.get(name) {
Some(inputs) if i < inputs.length() =>
self.expression_as(arg.value, depth + 1, inputs[i].scalar)
_ => self.expression(arg.value, depth + 1)
},
)
i += 1
}
match qualified {
Some((package_alias, source_name)) =>
QualifiedCall(package_alias, source_name, arguments, source)
None => Call(name, arguments, source)
}
}
_ => {
reject(
"unsupported_expression", "Expected a pure Boolean library expression", loc,
)
abort("unreachable")
}
}
}
///|
/// Complete signatures precede body adaptation, enabling forward references.
fn library_adapt(
parsed : ParsedLibrary,
limits : LibraryLimits,
cancelled : () -> Bool,
channel : SourceChannel,
) -> Array[LibraryFunction] raise {
let nodes = parsed.nodes
if nodes.is_empty() || nodes.length() > limits.max_functions {
library_error(
"declaration_count",
"Expected one or more functions within the declaration budget",
None,
)
}
let signatures = []
let names : Map[String, Bool] = Map([])
for node in nodes {
library_poll(cancelled)
guard node
is @ast.TopFuncDef(fun_decl=decl, decl_body=body, where_clause=None, ..) else {
reject(
"unsupported_declaration",
"Only top-level functions are supported",
node.loc(),
)
abort("unreachable")
}
if decl.type_name is Some(_) ||
decl.is_async is Some(_) ||
decl.nocancel_ is Some(_) ||
!decl.quantifiers.is_empty() ||
!decl.attrs.is_empty() {
reject(
"unsupported_function",
"Methods, generics, attributes and effects are unsupported",
decl.loc,
)
}
let effect = match decl.error_type {
@ast.NoErrorType => LibraryEffect::Pure
@ast.ErrorType(
ty=@ast.Name(
constr_id={ id: @ast.Dot(pkg="sdk", id="SdkError"), .. },
tys~,
..
)
) if tys.is_empty() => SdkError
@ast.ErrorType(
ty=@ast.Name(
constr_id={ id: @ast.Dot(pkg="decimal", id="DecimalError"), .. },
tys~,
..
)
) if tys.is_empty() => DecimalError
_ => {
reject(
"unsupported_function",
"Only the pinned raise @sdk.SdkError declaration is supported in the numeric SDK profile",
decl.loc,
)
abort("unreachable")
}
}
if channel == Stable && effect != Pure {
library_error(
"experimental_capability",
"SDK error declarations require " + EXPERIMENTAL_PROFILE,
Some(span(decl.loc)),
)
}
let public = match decl.vis {
@ast.Pub(attr=None, ..) => true
@ast.Default => false
_ => {
reject(
"unsupported_visibility",
"Only default or plain pub function visibility is supported",
decl.loc,
)
abort("unreachable")
}
}
if names.contains(decl.name.name) {
reject(
"duplicate_function",
"Duplicate function: " + decl.name.name,
decl.loc,
)
}
names[decl.name.name] = true
let output_type = library_type(
decl.return_type,
decl.return_type.map(t => t.loc()).unwrap_or(decl.loc),
channel,
)
guard decl.decl_params is Some(parameters) else {
reject(
"unsupported_parameters",
"An explicit positional parameter list is required",
decl.loc,
)
abort("unreachable")
}
if parameters.length() > limits.max_parameters {
reject("parameter_limit", "Parameter budget exceeded", decl.params_loc)
}
let parameter_names : Map[String, Bool] = Map([])
let inputs = []
let input_types = []
for parameter in parameters {
guard parameter is @ast.Positional(binder~, ty~) else {
reject(
"unsupported_parameters",
"Only named positional parameters are supported",
parameter.loc(),
)
abort("unreachable")
}
input_types.push(
library_type(ty, ty.map(t => t.loc()).unwrap_or(binder.loc), channel),
)
if parameter_names.contains(binder.name) {
reject(
"duplicate_parameter",
"Duplicate parameter: " + binder.name,
binder.loc,
)
}
parameter_names[binder.name] = true
inputs.push((binder.name, span(binder.loc)))
}
guard body is @ast.DeclBody(expr~) else {
reject(
"missing_body",
"An ordinary expression body is required",
decl.loc,
)
abort("unreachable")
}
signatures.push(
(
decl.name.name,
public,
inputs,
input_types,
output_type,
expr,
span(decl.name.loc),
source_documentation(decl.doc),
effect,
),
)
}
let signature_types : Map[String, Array[LibraryTypeUse]] = Map([])
for (name, _, _, input_types, _, _, _, _, _) in signatures {
signature_types[name] = input_types
}
let adapter = LibraryAdapter::{
source_chars: parsed.source_chars,
channel,
signatures: signature_types,
limits,
cancelled,
expressions: 0,
calls: 0,
lets: 0,
}
let functions = []
for
(
name,
public,
parameters,
input_types,
output_type,
body,
source,
doc,
effect,
) in signatures {
adapter.lets = 0
functions.push({
name,
doc,
public,
parameters,
input_types,
output_type,
effect,
body: adapter.expression_as(body, 0, output_type.scalar),
body_source: span(body.loc()),
source,
})
}
functions
}