641 lines
23 KiB
Rust
641 lines
23 KiB
Rust
use crate::diagnostic::Diagnostic;
|
|
use crate::intrinsic::Intrinsic;
|
|
use crate::source::Span;
|
|
use crate::types::{Field, HandlerSignature, Shape, Type};
|
|
|
|
use super::{Checked, Context};
|
|
|
|
pub(super) struct Signature {
|
|
pub handler: Option<HandlerSignature>,
|
|
pub parameters: Vec<Type>,
|
|
pub output: Type,
|
|
}
|
|
|
|
pub(super) fn signature(
|
|
operation: Intrinsic,
|
|
context: &Context<'_>,
|
|
span: Span,
|
|
) -> Checked<Signature> {
|
|
use Intrinsic::*;
|
|
use Type::*;
|
|
let mut handler = None;
|
|
let result = |name: &str, success: &str, payload: &[Type], eof: bool| -> Checked<Type> {
|
|
let id = context.named_type(name, span)?;
|
|
let Shape::Choice(variants) = &context.types[id].shape else {
|
|
return Err(Diagnostic::error(
|
|
"C005",
|
|
"system result must be a choice type",
|
|
span,
|
|
));
|
|
};
|
|
let mut expected = vec![(success, payload.to_vec()), ("Failed", vec![Text])];
|
|
if eof {
|
|
expected.push(("End", vec![]));
|
|
}
|
|
if variants.len() != expected.len()
|
|
|| !expected.iter().all(|(name, payload)| {
|
|
variants.iter().any(|variant| {
|
|
variant.name == *name
|
|
&& variant
|
|
.fields
|
|
.iter()
|
|
.map(|field| &field.ty)
|
|
.eq(payload.iter())
|
|
})
|
|
})
|
|
{
|
|
return Err(Diagnostic::error(
|
|
"C005",
|
|
"official result type does not match its system primitive contract",
|
|
span,
|
|
));
|
|
}
|
|
Ok(Named(id))
|
|
};
|
|
let (parameters, output) = match operation {
|
|
TestAssert | TestAssertEq | TestSecret | AuthOwnsUser => {
|
|
unreachable!("polymorphic primitives are checked contextually")
|
|
}
|
|
CryptoToken => (
|
|
vec![Int],
|
|
result("dever.crypto.TokenResult", "Ready", &[Secret], false)?,
|
|
),
|
|
CryptoPasswordHash => (
|
|
vec![Secret],
|
|
result("dever.crypto.HashResult", "Ready", &[Text], false)?,
|
|
),
|
|
CryptoPasswordVerify => (
|
|
vec![Secret, Text],
|
|
result("dever.crypto.VerifyResult", "Ready", &[Bool], false)?,
|
|
),
|
|
CryptoSha256 => (vec![Bytes], Bytes),
|
|
CryptoHmacSha256 => (vec![Secret, Bytes], Bytes),
|
|
CryptoConstantTimeEq => (vec![Bytes, Bytes], Bool),
|
|
ApiServe => (vec![], Unit),
|
|
UploadFilename | UploadContentType => (vec![Upload], Text),
|
|
UploadSize => (vec![Upload], Int),
|
|
UploadClose => (vec![Upload], Unit),
|
|
UploadStore => (
|
|
vec![Upload],
|
|
result("dever.storage.PutResult", "Stored", &[Uuid], false)?,
|
|
),
|
|
ApiRequestId | ApiMethod | ApiPath => (vec![], Text),
|
|
ApiClientAddress => (vec![], Text.nullable()),
|
|
ApiHeader | ApiCookie => (vec![Text], Text.nullable()),
|
|
ApiSecretCookie => (vec![Text], Secret.nullable()),
|
|
ApiResponseHeader => (vec![Text, Text], Unit),
|
|
ApiResponseCookie | ApiResponseSecretCookie => {
|
|
let options = context.named_type("dever.api.CookieOptions", span)?;
|
|
let value = if matches!(operation, ApiResponseCookie) {
|
|
Text
|
|
} else {
|
|
Secret
|
|
};
|
|
(vec![Text, value, Named(options)], Unit)
|
|
}
|
|
AuthIssue => (vec![Text, Text, Text.nullable()], Secret),
|
|
AuthIssueCookie => (vec![Text, Text, Text.nullable()], Unit),
|
|
AuthClearCookie => (vec![], Unit),
|
|
AuthId | AuthSession | SiteKey => (vec![], Text),
|
|
AuthUserId | AuthTenantId => (vec![], Int.nullable()),
|
|
AuthPermissions => {
|
|
let permission = context.named_type("dever.auth.Permission", span)?;
|
|
(vec![], List(Box::new(Named(permission))))
|
|
}
|
|
AuthSaveRole => (vec![Text, Text, Bool, List(Box::new(Text))], Unit),
|
|
AuthGrantRole | AuthRevokeRole => (vec![Int, Text], Unit),
|
|
AuthDisableRole => (vec![Text], Unit),
|
|
JobServe => {
|
|
if !context.symbols.packages[context.owner].bundled
|
|
&& !matches!(
|
|
context.symbols.packages[context.owner].layout,
|
|
crate::source::SourceLayout::Main
|
|
)
|
|
{
|
|
return Err(Diagnostic::error(
|
|
"C006",
|
|
"only main may start the Job worker",
|
|
span,
|
|
));
|
|
}
|
|
(vec![], Unit)
|
|
}
|
|
TestJobDrain | TestClockAdvance => {
|
|
if !context.is_test() {
|
|
return Err(Diagnostic::error(
|
|
"C006",
|
|
"Job test controls are available only in test sources",
|
|
span,
|
|
));
|
|
}
|
|
(
|
|
vec![Int],
|
|
if matches!(operation, TestJobDrain) {
|
|
Int
|
|
} else {
|
|
Unit
|
|
},
|
|
)
|
|
}
|
|
TaskTimeout | TaskRace | StreamOf => unreachable!("polymorphic core operation"),
|
|
TaskTicks => (vec![Int], AsyncStream(Box::new(Int))),
|
|
TextTrim | TextLower | TextUpper => (vec![Text], Text),
|
|
TextCodepoint => (vec![Text], Int.nullable()),
|
|
TextFromCodepoint => (vec![Int], Text.nullable()),
|
|
TextAt => (vec![Text, Int], Text.nullable()),
|
|
TextSlice => (vec![Text, Int, Int], Text.nullable()),
|
|
TextIndexOf => (vec![Text, Text], Int.nullable()),
|
|
TextContains | TextStartsWith | TextEndsWith => (vec![Text, Text], Bool),
|
|
TextSplit => (vec![Text, Text], List(Box::new(Text))),
|
|
TextReplace => (vec![Text, Text, Text], Text),
|
|
IntParse => (vec![Text], Int.nullable()),
|
|
IntToText => (vec![Int], Text),
|
|
DecimalParse => (vec![Text], Decimal.nullable()),
|
|
DecimalToText => (vec![Decimal], Text),
|
|
DecimalFromInt => (vec![Int], Decimal),
|
|
FloatParse => (vec![Text], Float.nullable()),
|
|
FloatToText => (vec![Float], Text),
|
|
ProcessArguments => (
|
|
vec![],
|
|
result(
|
|
"dever.process.ArgumentsResult",
|
|
"Read",
|
|
&[List(Box::new(Text))],
|
|
false,
|
|
)?,
|
|
),
|
|
TimeUnixMillis | TimeMonotonicNanos => (
|
|
vec![],
|
|
result("dever.time.ClockResult", "Read", &[Int], false)?,
|
|
),
|
|
TimeNow => (
|
|
vec![],
|
|
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
|
),
|
|
TimeParseDateTime => (
|
|
vec![Text],
|
|
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
|
),
|
|
TimeParseDate => (
|
|
vec![Text],
|
|
result("dever.time.DateResult", "Read", &[Date], false)?,
|
|
),
|
|
TimeParseTime => (
|
|
vec![Text],
|
|
result("dever.time.TimeResult", "Read", &[Time], false)?,
|
|
),
|
|
TimeFormatDateTime => (
|
|
vec![DateTime],
|
|
result("dever.time.TextResult", "Read", &[Text], false)?,
|
|
),
|
|
TimeFormatDate => (
|
|
vec![Date],
|
|
result("dever.time.TextResult", "Read", &[Text], false)?,
|
|
),
|
|
TimeFormatTime => (
|
|
vec![Time],
|
|
result("dever.time.TextResult", "Read", &[Text], false)?,
|
|
),
|
|
TimeDuration => (vec![Int], Duration),
|
|
TimeAdd | TimeSubtract => (
|
|
vec![DateTime, Duration],
|
|
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
|
),
|
|
TimeDifference => (
|
|
vec![DateTime, DateTime],
|
|
result("dever.time.DurationResult", "Read", &[Duration], false)?,
|
|
),
|
|
TimeSleep | TaskSleep => (
|
|
vec![Int],
|
|
result("dever.time.SleepResult", "Done", &[], false)?,
|
|
),
|
|
DecimalRound => (vec![Decimal, Int], Decimal),
|
|
FloatFromInt => (vec![Int], Float),
|
|
FloatSqrt | FloatSin | FloatCos | FloatLog => (vec![Float], Float),
|
|
FloatPow => (vec![Float, Float], Float),
|
|
FloatIsNan | FloatIsFinite | FloatIsInfinite => (vec![Float], Bool),
|
|
StdoutWrite => (vec![Text], Unit),
|
|
LogDebug | LogInfo | LogWarn | LogError => {
|
|
(vec![Text, Map(Box::new(Text), Box::new(Text))], Unit)
|
|
}
|
|
IdFromText => (vec![Text], Id),
|
|
IdToText => (vec![Id], Text),
|
|
UuidParse => (vec![Text], Uuid.nullable()),
|
|
UuidToText => (vec![Uuid], Text),
|
|
BytesFromText => (vec![Text], Bytes),
|
|
BytesToText => (
|
|
vec![Bytes],
|
|
result("dever.bytes.DecodeResult", "Decoded", &[Text], false)?,
|
|
),
|
|
BytesFromInts => (
|
|
vec![List(Box::new(Int))],
|
|
result("dever.bytes.BufferResult", "Ready", &[Bytes], false)?,
|
|
),
|
|
BytesLength => (vec![Bytes], Int),
|
|
BytesAt => (vec![Bytes, Int], Int.nullable()),
|
|
BytesSlice => (
|
|
vec![Bytes, Int, Int],
|
|
result("dever.bytes.BufferResult", "Ready", &[Bytes], false)?,
|
|
),
|
|
BytesConcat => (vec![Bytes, Bytes], Bytes),
|
|
FileOpen | FileCreate => (
|
|
vec![Text],
|
|
result("dever.io.OpenResult", "Opened", &[File], false)?,
|
|
),
|
|
FileRead => (
|
|
vec![File, Int],
|
|
result("dever.io.SystemReadResult", "Read", &[Bytes], true)?,
|
|
),
|
|
FileWrite => (
|
|
vec![File, Bytes],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
),
|
|
FileClose => (
|
|
vec![File],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
FileChunks | TcpChunks => {
|
|
let event = result("dever.io.ReadEvent", "Chunk", &[Bytes], false)?;
|
|
let (source, stream, result_name) = if matches!(operation, FileChunks) {
|
|
(File, Stream(Box::new(event)), "dever.io.ReadStreamResult")
|
|
} else {
|
|
(
|
|
Socket,
|
|
AsyncStream(Box::new(event)),
|
|
"dever.net.ReadStreamResult",
|
|
)
|
|
};
|
|
(
|
|
vec![source, Int],
|
|
result(result_name, "Streaming", &[stream], false)?,
|
|
)
|
|
}
|
|
TcpConnect => (
|
|
vec![Text, Int],
|
|
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
|
),
|
|
TcpConnectTimeout => (
|
|
vec![Text, Int, Int],
|
|
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
|
),
|
|
TcpListen => (
|
|
vec![Text, Int],
|
|
result("dever.net.ListenResult", "Listening", &[Listener], false)?,
|
|
),
|
|
TcpAccept => (
|
|
vec![Listener],
|
|
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
|
),
|
|
TcpPort => (
|
|
vec![Listener],
|
|
result("dever.net.PortResult", "Bound", &[Int], false)?,
|
|
),
|
|
TcpRead => (
|
|
vec![Socket, Int],
|
|
result("dever.net.SystemReadResult", "Read", &[Bytes], true)?,
|
|
),
|
|
TcpWrite => (
|
|
vec![Socket, Bytes],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
),
|
|
TcpTimeout => (
|
|
vec![Socket, Int],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
),
|
|
TcpClose => (
|
|
vec![Socket],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
TcpCloseListener => (
|
|
vec![Listener],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
TcpConnections => {
|
|
let connection = result("dever.net.ConnectResult", "Connected", &[Socket], false)?;
|
|
(vec![Listener], AsyncStream(Box::new(connection)))
|
|
}
|
|
TlsSystem => (vec![], ClientTls),
|
|
TlsClient => (
|
|
vec![Bytes],
|
|
result("dever.tls.ClientResult", "Ready", &[ClientTls], false)?,
|
|
),
|
|
TlsServer => (
|
|
vec![Bytes, Bytes],
|
|
result("dever.tls.ServerResult", "Ready", &[ServerTls], false)?,
|
|
),
|
|
HttpCloseClient => (
|
|
vec![HttpClient],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
HttpServe | HttpServeTls | HttpSend | HttpServeLive | HttpServeLiveTls | HttpRespond
|
|
| HttpStart | SseStart | HttpClientNew | HttpRequest | HttpOpen | HttpOpenStream => {
|
|
let header = record(
|
|
context,
|
|
"dever.http.Header",
|
|
&[("name", Text), ("value", Bytes)],
|
|
span,
|
|
)?;
|
|
let headers = List(Box::new(header));
|
|
let request = record(
|
|
context,
|
|
"dever.http.Request",
|
|
&[
|
|
("method", Text),
|
|
("target", Text),
|
|
("headers", headers.clone()),
|
|
("body", Bytes),
|
|
],
|
|
span,
|
|
)?;
|
|
let response = record(
|
|
context,
|
|
"dever.http.Response",
|
|
&[
|
|
("status", Int),
|
|
("headers", headers.clone()),
|
|
("body", Bytes),
|
|
],
|
|
span,
|
|
)?;
|
|
let http2 = record(
|
|
context,
|
|
"dever.http.Http2Limits",
|
|
&[
|
|
("streams", Int),
|
|
("stream_window_bytes", Int),
|
|
("connection_window_bytes", Int),
|
|
],
|
|
span,
|
|
)?;
|
|
let limits = record(
|
|
context,
|
|
"dever.http.Limits",
|
|
&[
|
|
("header_bytes", Int),
|
|
("body_bytes", Int),
|
|
("timeout_ms", Int),
|
|
("connections", Int),
|
|
("http2", http2.nullable()),
|
|
],
|
|
span,
|
|
)?;
|
|
match operation {
|
|
HttpServeLive | HttpServeLiveTls => {
|
|
let live = record(
|
|
context,
|
|
"dever.http.LiveLimits",
|
|
&[
|
|
("chunk_bytes", Int),
|
|
("idle_ms", Int),
|
|
("heartbeat_ms", Int),
|
|
],
|
|
span,
|
|
)?;
|
|
handler = Some(HandlerSignature {
|
|
parameters: vec![request, HttpReply],
|
|
bounds: vec![Default::default(); 2],
|
|
outputs: vec![],
|
|
});
|
|
let mut parameters = vec![Listener, limits, live];
|
|
if matches!(operation, HttpServeLiveTls) {
|
|
parameters.push(ServerTls);
|
|
}
|
|
(parameters, Unit)
|
|
}
|
|
HttpServe | HttpServeTls => {
|
|
handler = Some(HandlerSignature {
|
|
parameters: vec![request],
|
|
bounds: vec![Default::default()],
|
|
outputs: vec![Field {
|
|
name: "response".into(),
|
|
ty: response,
|
|
private: false,
|
|
bounds: Default::default(),
|
|
}],
|
|
});
|
|
let mut parameters = vec![Listener, limits];
|
|
if matches!(operation, HttpServeTls) {
|
|
parameters.push(ServerTls);
|
|
}
|
|
(parameters, Unit)
|
|
}
|
|
HttpRespond | HttpStart | SseStart => {
|
|
let parameters = match operation {
|
|
HttpRespond => vec![HttpReply, response],
|
|
HttpStart => vec![HttpReply, Int, headers],
|
|
SseStart => vec![HttpReply, headers],
|
|
_ => unreachable!(),
|
|
};
|
|
(
|
|
parameters,
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
)
|
|
}
|
|
HttpClientNew => {
|
|
let pool = record(
|
|
context,
|
|
"dever.http.PoolLimits",
|
|
&[("idle_ms", Int), ("chunk_bytes", Int), ("read_ms", Int)],
|
|
span,
|
|
)?;
|
|
(
|
|
vec![Text, ClientTls, limits, pool],
|
|
result("dever.http.ClientResult", "Ready", &[HttpClient], false)?,
|
|
)
|
|
}
|
|
HttpSend => (
|
|
vec![Text, Int, request, limits],
|
|
result("dever.http.ResponseResult", "Read", &[response], false)?,
|
|
),
|
|
HttpRequest => (
|
|
vec![HttpClient, request],
|
|
result("dever.http.ResponseResult", "Read", &[response], false)?,
|
|
),
|
|
HttpOpen | HttpOpenStream => {
|
|
let event = result("dever.io.ReadEvent", "Chunk", &[Bytes], false)?;
|
|
let body = AsyncStream(Box::new(event));
|
|
let response = record(
|
|
context,
|
|
"dever.http.StreamResponse",
|
|
&[
|
|
("status", Int),
|
|
("headers", headers.clone()),
|
|
("body", body.clone()),
|
|
],
|
|
span,
|
|
)?;
|
|
let request = if matches!(operation, HttpOpenStream) {
|
|
record(
|
|
context,
|
|
"dever.http.StreamRequest",
|
|
&[
|
|
("method", Text),
|
|
("target", Text),
|
|
("headers", headers),
|
|
("body", body),
|
|
],
|
|
span,
|
|
)?
|
|
} else {
|
|
request
|
|
};
|
|
(
|
|
vec![HttpClient, request],
|
|
result(
|
|
"dever.http.StreamResponseResult",
|
|
"Read",
|
|
&[response],
|
|
false,
|
|
)?,
|
|
)
|
|
}
|
|
_ => unreachable!("HTTP record operation"),
|
|
}
|
|
}
|
|
HttpWrite => (
|
|
vec![HttpReply, Bytes],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
),
|
|
HttpFinish => (
|
|
vec![HttpReply],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
SseSend => {
|
|
let event = record(
|
|
context,
|
|
"dever.sse.Event",
|
|
&[
|
|
("event", Text),
|
|
("data", Text),
|
|
("id", Text.nullable()),
|
|
("retry_ms", Int.nullable()),
|
|
],
|
|
span,
|
|
)?;
|
|
(
|
|
vec![HttpReply, event],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
)
|
|
}
|
|
WsAccept | WsConnect | WsOpen => {
|
|
let limits = record(
|
|
context,
|
|
"dever.websocket.Limits",
|
|
&[("message_bytes", Int), ("idle_ms", Int), ("write_ms", Int)],
|
|
span,
|
|
)?;
|
|
let parameters = match operation {
|
|
WsAccept => vec![HttpReply, limits],
|
|
WsOpen => vec![Text, ClientTls, limits],
|
|
_ => vec![Text, Int, Text, limits],
|
|
};
|
|
(
|
|
parameters,
|
|
result(
|
|
"dever.websocket.ConnectResult",
|
|
"Connected",
|
|
&[WebSocket],
|
|
false,
|
|
)?,
|
|
)
|
|
}
|
|
WsSend | WsReceive | WsMessages => {
|
|
let message = websocket_message(context, span)?;
|
|
match operation {
|
|
WsSend => (
|
|
vec![WebSocket, message],
|
|
result("dever.io.WriteResult", "Done", &[], false)?,
|
|
),
|
|
WsReceive => (
|
|
vec![WebSocket],
|
|
result(
|
|
"dever.websocket.SystemReceiveResult",
|
|
"Read",
|
|
&[message],
|
|
true,
|
|
)?,
|
|
),
|
|
WsMessages => {
|
|
let event = result("dever.websocket.MessageEvent", "Read", &[message], false)?;
|
|
(vec![WebSocket], AsyncStream(Box::new(event)))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
WsClose => (
|
|
vec![WebSocket, Int, Text],
|
|
result("dever.io.CloseResult", "Closed", &[], false)?,
|
|
),
|
|
};
|
|
Ok(Signature {
|
|
handler,
|
|
parameters,
|
|
output,
|
|
})
|
|
}
|
|
|
|
fn record(
|
|
context: &Context<'_>,
|
|
name: &str,
|
|
expected: &[(&str, Type)],
|
|
span: Span,
|
|
) -> Checked<Type> {
|
|
let id = context.named_type(name, span)?;
|
|
let valid = match &context.types[id].shape {
|
|
Shape::Record(fields) => {
|
|
fields.len() == expected.len()
|
|
&& expected.iter().all(|(name, ty)| {
|
|
fields.iter().any(|field| {
|
|
field.name == *name
|
|
&& field.ty == *ty
|
|
&& !field.private
|
|
&& field.bounds.is_empty()
|
|
})
|
|
})
|
|
}
|
|
Shape::Choice(_) => false,
|
|
};
|
|
if !valid {
|
|
return Err(Diagnostic::error(
|
|
"C005",
|
|
format!("'{name}' does not match its system record contract"),
|
|
span,
|
|
));
|
|
}
|
|
Ok(Type::Named(id))
|
|
}
|
|
|
|
fn websocket_message(context: &Context<'_>, span: Span) -> Checked<Type> {
|
|
let id = context.named_type("dever.websocket.Message", span)?;
|
|
let expected = [
|
|
("Text", Type::Text),
|
|
("Binary", Type::Bytes),
|
|
("Ping", Type::Bytes),
|
|
("Pong", Type::Bytes),
|
|
];
|
|
let valid = match &context.types[id].shape {
|
|
Shape::Choice(variants) => {
|
|
variants.len() == expected.len()
|
|
&& expected.iter().all(|(name, ty)| {
|
|
variants.iter().any(|variant| {
|
|
variant.name == *name
|
|
&& !variant.error
|
|
&& variant.fields.len() == 1
|
|
&& variant.fields[0].ty == *ty
|
|
&& variant.fields[0].bounds.is_empty()
|
|
&& !variant.fields[0].private
|
|
})
|
|
})
|
|
}
|
|
_ => false,
|
|
};
|
|
if !valid {
|
|
return Err(Diagnostic::error(
|
|
"C005",
|
|
"WebSocket Message does not match its system contract",
|
|
span,
|
|
));
|
|
}
|
|
Ok(Type::Named(id))
|
|
}
|