Compare commits
6 Commits
grackle-la
...
path-to-no
Author | SHA1 | Date | |
---|---|---|---|
7a7a83c835 | |||
de63e4f19f | |||
b70b271e6b | |||
08b7cdc5f6 | |||
6efe6b54c0 | |||
69f72d62e0 |
@ -48,6 +48,72 @@ type Timeout = ReturnType<typeof setTimeout>
|
|||||||
|
|
||||||
type ClientMetrics = Models['ClientMetrics_type']
|
type ClientMetrics = Models['ClientMetrics_type']
|
||||||
|
|
||||||
|
type Value<T, U> = U extends undefined
|
||||||
|
? { type: T; value: U }
|
||||||
|
: U extends void
|
||||||
|
? { type: T }
|
||||||
|
: { type: T; value: U }
|
||||||
|
|
||||||
|
type State<T, U> = Value<T, U>
|
||||||
|
|
||||||
|
enum EngineConnectionStateType {
|
||||||
|
Fresh = 'fresh',
|
||||||
|
Connecting = 'connecting',
|
||||||
|
ConnectionEstablished = 'connection-established',
|
||||||
|
Disconnected = 'disconnected',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DisconnectedType {
|
||||||
|
Error = 'error',
|
||||||
|
Timeout = 'timeout',
|
||||||
|
Quit = 'quit',
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisconnectedValue =
|
||||||
|
| State<DisconnectedType.Error, Error | undefined>
|
||||||
|
| State<DisconnectedType.Timeout, void>
|
||||||
|
| State<DisconnectedType.Quit, void>
|
||||||
|
|
||||||
|
// These are ordered by the expected sequence.
|
||||||
|
enum ConnectingType {
|
||||||
|
WebSocketConnecting = 'websocket-connecting',
|
||||||
|
WebSocketEstablished = 'websocket-established',
|
||||||
|
PeerConnectionCreated = 'peer-connection-created',
|
||||||
|
ICEServersSet = 'ice-servers-set',
|
||||||
|
SetLocalDescription = 'set-local-description',
|
||||||
|
OfferedSdp = 'offered-sdp',
|
||||||
|
ReceivedSdp = 'received-sdp',
|
||||||
|
SetRemoteDescription = 'set-remote-description',
|
||||||
|
WebRTCConnecting = 'webrtc-connecting',
|
||||||
|
ICECandidateReceived = 'ice-candidate-received',
|
||||||
|
TrackReceived = 'track-received',
|
||||||
|
DataChannelRequested = 'data-channel-requested',
|
||||||
|
DataChannelConnecting = 'data-channel-connecting',
|
||||||
|
DataChannelEstablished = 'data-channel-established',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectingValue =
|
||||||
|
| State<ConnectingType.WebSocketConnecting, void>
|
||||||
|
| State<ConnectingType.WebSocketEstablished, void>
|
||||||
|
| State<ConnectingType.PeerConnectionCreated, void>
|
||||||
|
| State<ConnectingType.ICEServersSet, void>
|
||||||
|
| State<ConnectingType.SetLocalDescription, void>
|
||||||
|
| State<ConnectingType.OfferedSdp, void>
|
||||||
|
| State<ConnectingType.ReceivedSdp, void>
|
||||||
|
| State<ConnectingType.SetRemoteDescription, void>
|
||||||
|
| State<ConnectingType.WebRTCConnecting, void>
|
||||||
|
| State<ConnectingType.TrackReceived, void>
|
||||||
|
| State<ConnectingType.ICECandidateReceived, void>
|
||||||
|
| State<ConnectingType.DataChannelRequested, string>
|
||||||
|
| State<ConnectingType.DataChannelConnecting, string>
|
||||||
|
| State<ConnectingType.DataChannelEstablished, void>
|
||||||
|
|
||||||
|
type EngineConnectionState =
|
||||||
|
| State<EngineConnectionStateType.Fresh, void>
|
||||||
|
| State<EngineConnectionStateType.Connecting, ConnectingValue>
|
||||||
|
| State<EngineConnectionStateType.ConnectionEstablished, void>
|
||||||
|
| State<EngineConnectionStateType.Disconnected, DisconnectedValue>
|
||||||
|
|
||||||
// EngineConnection encapsulates the connection(s) to the Engine
|
// EngineConnection encapsulates the connection(s) to the Engine
|
||||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||||
// and WebRTC connections.
|
// and WebRTC connections.
|
||||||
@ -55,10 +121,28 @@ class EngineConnection {
|
|||||||
websocket?: WebSocket
|
websocket?: WebSocket
|
||||||
pc?: RTCPeerConnection
|
pc?: RTCPeerConnection
|
||||||
unreliableDataChannel?: RTCDataChannel
|
unreliableDataChannel?: RTCDataChannel
|
||||||
|
mediaStream?: MediaStream
|
||||||
|
|
||||||
|
private _state: EngineConnectionState = {
|
||||||
|
type: EngineConnectionStateType.Fresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
get state(): EngineConnectionState {
|
||||||
|
return this._state
|
||||||
|
}
|
||||||
|
|
||||||
|
set state(next: EngineConnectionState) {
|
||||||
|
console.log(`${JSON.stringify(this.state)} → ${JSON.stringify(next)}`)
|
||||||
|
if (next.type === EngineConnectionStateType.Disconnected) {
|
||||||
|
console.trace()
|
||||||
|
const sub = next.value
|
||||||
|
if (sub.type === DisconnectedType.Error) {
|
||||||
|
console.error(sub.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._state = next
|
||||||
|
}
|
||||||
|
|
||||||
private ready: boolean
|
|
||||||
private connecting: boolean
|
|
||||||
private dead: boolean
|
|
||||||
private failedConnTimeout: Timeout | null
|
private failedConnTimeout: Timeout | null
|
||||||
|
|
||||||
readonly url: string
|
readonly url: string
|
||||||
@ -94,74 +178,77 @@ class EngineConnection {
|
|||||||
}) {
|
}) {
|
||||||
this.url = url
|
this.url = url
|
||||||
this.token = token
|
this.token = token
|
||||||
this.ready = false
|
|
||||||
this.connecting = false
|
|
||||||
this.dead = false
|
|
||||||
this.failedConnTimeout = null
|
this.failedConnTimeout = null
|
||||||
this.onWebsocketOpen = onWebsocketOpen
|
this.onWebsocketOpen = onWebsocketOpen
|
||||||
this.onDataChannelOpen = onDataChannelOpen
|
this.onDataChannelOpen = onDataChannelOpen
|
||||||
this.onEngineConnectionOpen = onEngineConnectionOpen
|
this.onEngineConnectionOpen = onEngineConnectionOpen
|
||||||
this.onConnectionStarted = onConnectionStarted
|
this.onConnectionStarted = onConnectionStarted
|
||||||
|
|
||||||
this.onClose = onClose
|
this.onClose = onClose
|
||||||
this.onNewTrack = onNewTrack
|
this.onNewTrack = onNewTrack
|
||||||
|
|
||||||
// TODO(paultag): This ought to be tweakable.
|
// TODO(paultag): This ought to be tweakable.
|
||||||
const pingIntervalMs = 10000
|
const pingIntervalMs = 10000
|
||||||
|
|
||||||
|
// Without an interval ping, our connection will timeout.
|
||||||
let pingInterval = setInterval(() => {
|
let pingInterval = setInterval(() => {
|
||||||
if (this.dead) {
|
switch (this.state.type as EngineConnectionStateType) {
|
||||||
clearInterval(pingInterval)
|
case EngineConnectionStateType.ConnectionEstablished:
|
||||||
}
|
|
||||||
if (this.isReady()) {
|
|
||||||
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
|
||||||
// command through the WebSocket connection. This will help both ends
|
|
||||||
// of the connection maintain the TCP connection without hitting a
|
|
||||||
// timeout condition.
|
|
||||||
this.send({ type: 'ping' })
|
this.send({ type: 'ping' })
|
||||||
|
break
|
||||||
|
case EngineConnectionStateType.Disconnected:
|
||||||
|
clearInterval(pingInterval)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
let connectInterval = setInterval(() => {
|
let connectRetryInterval = setInterval(() => {
|
||||||
if (this.dead) {
|
if (this.state.type !== EngineConnectionStateType.Disconnected) return
|
||||||
clearInterval(connectInterval)
|
switch (this.state.value.type) {
|
||||||
return
|
case DisconnectedType.Error:
|
||||||
}
|
clearInterval(connectRetryInterval)
|
||||||
if (this.isReady()) {
|
break
|
||||||
return
|
case DisconnectedType.Timeout:
|
||||||
}
|
console.log('Trying to reconnect')
|
||||||
console.log('connecting via retry')
|
|
||||||
this.connect()
|
this.connect()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}, connectionTimeoutMs)
|
}, connectionTimeoutMs)
|
||||||
}
|
}
|
||||||
// isConnecting will return true when connect has been called, but the full
|
|
||||||
// WebRTC is not online.
|
|
||||||
isConnecting() {
|
isConnecting() {
|
||||||
return this.connecting
|
return this.state.type === EngineConnectionStateType.Connecting
|
||||||
}
|
}
|
||||||
// isReady will return true only when the WebRTC *and* WebSocket connection
|
|
||||||
// are connected. During setup, the WebSocket connection comes online first,
|
|
||||||
// which is used to establish the WebRTC connection. The EngineConnection
|
|
||||||
// is not "Ready" until both are connected.
|
|
||||||
isReady() {
|
isReady() {
|
||||||
return this.ready
|
return this.state.type === EngineConnectionStateType.ConnectionEstablished
|
||||||
}
|
}
|
||||||
|
|
||||||
tearDown() {
|
tearDown() {
|
||||||
this.dead = true
|
this.disconnectAll()
|
||||||
this.close()
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: { type: DisconnectedType.Quit },
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// shouldTrace will return true when Sentry should be used to instrument
|
// shouldTrace will return true when Sentry should be used to instrument
|
||||||
// the Engine.
|
// the Engine.
|
||||||
shouldTrace() {
|
shouldTrace() {
|
||||||
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
|
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect will attempt to connect to the Engine over a WebSocket, and
|
// connect will attempt to connect to the Engine over a WebSocket, and
|
||||||
// establish the WebRTC connections.
|
// establish the WebRTC connections.
|
||||||
//
|
//
|
||||||
// This will attempt the full handshake, and retry if the connection
|
// This will attempt the full handshake, and retry if the connection
|
||||||
// did not establish.
|
// did not establish.
|
||||||
connect() {
|
connect() {
|
||||||
console.log('connect was called')
|
|
||||||
if (this.isConnecting() || this.isReady()) {
|
if (this.isConnecting() || this.isReady()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -195,228 +282,98 @@ class EngineConnection {
|
|||||||
let handshakeSpan: SpanPromise
|
let handshakeSpan: SpanPromise
|
||||||
let iceSpan: SpanPromise
|
let iceSpan: SpanPromise
|
||||||
|
|
||||||
|
const spanStart = (op: string) =>
|
||||||
|
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
webrtcMediaTransaction = Sentry.startTransaction({
|
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
|
||||||
name: 'webrtc-media',
|
websocketSpan = spanStart('websocket')
|
||||||
})
|
|
||||||
websocketSpan = new SpanPromise(
|
|
||||||
webrtcMediaTransaction.startChild({ op: 'websocket' })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.websocket = new WebSocket(this.url, [])
|
const createPeerConnection = () => {
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
|
||||||
|
|
||||||
this.pc = new RTCPeerConnection()
|
this.pc = new RTCPeerConnection()
|
||||||
this.pc.createDataChannel('unreliable_modeling_cmds')
|
|
||||||
this.websocket.addEventListener('open', (event) => {
|
// Data channels MUST BE specified before SDP offers because requesting
|
||||||
console.log('Connected to websocket, waiting for ICE servers')
|
// them affects what our needs are!
|
||||||
if (this.token) {
|
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
||||||
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
this.pc.createDataChannel(DATACHANNEL_NAME_UMC)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.DataChannelRequested,
|
||||||
|
value: DATACHANNEL_NAME_UMC,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pc.addEventListener('icecandidate', (event) => {
|
||||||
|
if (event.candidate === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.ICECandidateReceived,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a candidate to use
|
||||||
|
this.send({
|
||||||
|
type: 'trickle_ice',
|
||||||
|
candidate: event.candidate.toJSON(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('icecandidateerror', (_event) => {
|
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
|
||||||
const event = _event as RTCPeerConnectionIceErrorEvent
|
const event = _event as RTCPeerConnectionIceErrorEvent
|
||||||
console.error(
|
console.warn(
|
||||||
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
|
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
|
||||||
if (this.pc?.iceConnectionState === 'connected') {
|
// Event type: generic Event type...
|
||||||
|
this.pc.addEventListener('connectionstatechange', (event: any) => {
|
||||||
|
console.log('connectionstatechange: ' + event.target?.connectionState)
|
||||||
|
switch (event.target?.connectionState) {
|
||||||
|
// From what I understand, only after have we done the ICE song and
|
||||||
|
// dance is it safest to connect the video tracks / stream
|
||||||
|
case 'connected':
|
||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
iceSpan.resolve?.()
|
iceSpan.resolve?.()
|
||||||
}
|
}
|
||||||
} else if (this.pc?.iceConnectionState === 'failed') {
|
|
||||||
// failed is a terminal state; let's explicitly kill the
|
// Let the browser attach to the video stream now
|
||||||
// connection to the server at this point.
|
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
|
||||||
console.log('failed to negotiate ice connection; restarting')
|
break
|
||||||
this.close()
|
case 'failed':
|
||||||
|
this.disconnectAll()
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: {
|
||||||
|
type: DisconnectedType.Error,
|
||||||
|
value: new Error(
|
||||||
|
'failed to negotiate ice connection; restarting'
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
break
|
||||||
|
default:
|
||||||
this.websocket.addEventListener('open', (event) => {
|
break
|
||||||
if (this.shouldTrace()) {
|
|
||||||
websocketSpan.resolve?.()
|
|
||||||
|
|
||||||
handshakeSpan = new SpanPromise(
|
|
||||||
webrtcMediaTransaction.startChild({ op: 'handshake' })
|
|
||||||
)
|
|
||||||
iceSpan = new SpanPromise(
|
|
||||||
webrtcMediaTransaction.startChild({ op: 'ice' })
|
|
||||||
)
|
|
||||||
dataChannelSpan = new SpanPromise(
|
|
||||||
webrtcMediaTransaction.startChild({
|
|
||||||
op: 'data-channel',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
mediaTrackSpan = new SpanPromise(
|
|
||||||
webrtcMediaTransaction.startChild({
|
|
||||||
op: 'media-track',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
|
||||||
Promise.all([
|
|
||||||
handshakeSpan.promise,
|
|
||||||
iceSpan.promise,
|
|
||||||
dataChannelSpan.promise,
|
|
||||||
mediaTrackSpan.promise,
|
|
||||||
]).then(() => {
|
|
||||||
console.log('All spans finished, reporting')
|
|
||||||
webrtcMediaTransaction?.finish()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onWebsocketOpen(this)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.websocket.addEventListener('close', (event) => {
|
|
||||||
console.log('websocket connection closed', event)
|
|
||||||
this.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.websocket.addEventListener('error', (event) => {
|
|
||||||
console.log('websocket connection error', event)
|
|
||||||
this.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.websocket.addEventListener('message', (event) => {
|
|
||||||
// In the EngineConnection, we're looking for messages to/from
|
|
||||||
// the server that relate to the ICE handshake, or WebRTC
|
|
||||||
// negotiation. There may be other messages (including ArrayBuffer
|
|
||||||
// messages) that are intended for the GUI itself, so be careful
|
|
||||||
// when assuming we're the only consumer or that all messages will
|
|
||||||
// be carefully formatted here.
|
|
||||||
|
|
||||||
if (typeof event.data !== 'string') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
|
||||||
|
|
||||||
if (!message.success) {
|
|
||||||
const errorsString = message?.errors
|
|
||||||
?.map((error) => {
|
|
||||||
return ` - ${error.error_code}: ${error.message}`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
if (message.request_id) {
|
|
||||||
console.error(
|
|
||||||
`Error in response to request ${message.request_id}:\n${errorsString}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
console.error(`Error from server:\n${errorsString}`)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = message.resp
|
|
||||||
if (!resp) {
|
|
||||||
// If there's no body to the response, we can bail here.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.type === 'sdp_answer') {
|
|
||||||
let answer = resp.data?.answer
|
|
||||||
if (!answer || answer.type === 'unspecified') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.pc?.signalingState !== 'stable') {
|
|
||||||
// If the connection is stable, we shouldn't bother updating the
|
|
||||||
// SDP, since we have a stable connection to the backend. If we
|
|
||||||
// need to renegotiate, the whole PeerConnection needs to get
|
|
||||||
// tore down.
|
|
||||||
this.pc?.setRemoteDescription(
|
|
||||||
new RTCSessionDescription({
|
|
||||||
type: answer.type,
|
|
||||||
sdp: answer.sdp,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
|
||||||
// When both ends have a local and remote SDP, we've been able to
|
|
||||||
// set up successfully. We'll still need to find the right ICE
|
|
||||||
// servers, but this is hand-shook.
|
|
||||||
handshakeSpan.resolve?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resp.type === 'trickle_ice') {
|
|
||||||
let candidate = resp.data?.candidate
|
|
||||||
this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
|
||||||
} else if (resp.type === 'ice_server_info' && this.pc) {
|
|
||||||
console.log('received ice_server_info')
|
|
||||||
let ice_servers = resp.data?.ice_servers
|
|
||||||
|
|
||||||
if (ice_servers?.length > 0) {
|
|
||||||
// When we set the Configuration, we want to always force
|
|
||||||
// iceTransportPolicy to 'relay', since we know the topology
|
|
||||||
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
|
||||||
// talk to the engine in any configuration /other/ than relay
|
|
||||||
// from a infra POV.
|
|
||||||
this.pc.setConfiguration({
|
|
||||||
iceServers: ice_servers,
|
|
||||||
iceTransportPolicy: 'relay',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.pc?.setConfiguration({})
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have an ICE Servers set now. We just setConfiguration, so let's
|
|
||||||
// start adding things we care about to the PeerConnection and let
|
|
||||||
// ICE negotiation happen in the background. Everything from here
|
|
||||||
// until the end of this function is setup of our end of the
|
|
||||||
// PeerConnection and waiting for events to fire our callbacks.
|
|
||||||
|
|
||||||
this.pc.addEventListener('icecandidate', (event) => {
|
|
||||||
if (!this.pc || !this.websocket) return
|
|
||||||
if (event.candidate !== null) {
|
|
||||||
console.log('sending trickle ice candidate')
|
|
||||||
const { candidate } = event
|
|
||||||
this.send({
|
|
||||||
type: 'trickle_ice',
|
|
||||||
candidate: candidate.toJSON(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Offer to receive 1 video track
|
|
||||||
this.pc.addTransceiver('video', {})
|
|
||||||
|
|
||||||
// Finally (but actually firstly!), to kick things off, we're going to
|
|
||||||
// generate our SDP, set it on our PeerConnection, and let the server
|
|
||||||
// know about our capabilities.
|
|
||||||
this.pc
|
|
||||||
.createOffer()
|
|
||||||
.then(async (descriptionInit) => {
|
|
||||||
await this?.pc?.setLocalDescription(descriptionInit)
|
|
||||||
console.log('sent sdp_offer begin')
|
|
||||||
this.send({
|
|
||||||
type: 'sdp_offer',
|
|
||||||
offer: this.pc?.localDescription,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(console.log)
|
|
||||||
} else if (resp.type === 'metrics_request') {
|
|
||||||
if (this.webrtcStatsCollector === undefined) {
|
|
||||||
// TODO: Error message here?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.webrtcStatsCollector().then((client_metrics) => {
|
|
||||||
this.send({
|
|
||||||
type: 'metrics_response',
|
|
||||||
metrics: client_metrics,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('track', (event) => {
|
this.pc.addEventListener('track', (event) => {
|
||||||
const mediaStream = event.streams[0]
|
const mediaStream = event.streams[0]
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.TrackReceived,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
|
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
|
||||||
mediaStreamTrack.addEventListener('unmute', () => {
|
mediaStreamTrack.addEventListener('unmute', () => {
|
||||||
@ -436,7 +393,7 @@ class EngineConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let videoTrack = mediaStream.getVideoTracks()[0]
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||||
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||||
let client_metrics: ClientMetrics = {
|
let client_metrics: ClientMetrics = {
|
||||||
rtc_frames_decoded: 0,
|
rtc_frames_decoded: 0,
|
||||||
rtc_frames_dropped: 0,
|
rtc_frames_dropped: 0,
|
||||||
@ -481,56 +438,357 @@ class EngineConnection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onNewTrack({
|
// The app is eager to use the MediaStream; as soon as onNewTrack is
|
||||||
conn: this,
|
// called, the following sequence happens:
|
||||||
mediaStream: mediaStream,
|
// EngineConnection.onNewTrack -> StoreState.setMediaStream ->
|
||||||
})
|
// Stream.tsx reacts to mediaStream change, setting a video element.
|
||||||
|
// We wait until connectionstatechange changes to "connected"
|
||||||
|
// to pass it to the rest of the application.
|
||||||
|
|
||||||
|
this.mediaStream = mediaStream
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('datachannel', (event) => {
|
this.pc.addEventListener('datachannel', (event) => {
|
||||||
this.unreliableDataChannel = event.channel
|
this.unreliableDataChannel = event.channel
|
||||||
|
|
||||||
console.log('accepted unreliable data channel', event.channel.label)
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.DataChannelConnecting,
|
||||||
|
value: event.channel.label,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('open', (event) => {
|
this.unreliableDataChannel.addEventListener('open', (event) => {
|
||||||
console.log('unreliable data channel opened', event)
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.DataChannelEstablished,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
dataChannelSpan.resolve?.()
|
dataChannelSpan.resolve?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onDataChannelOpen(this)
|
this.onDataChannelOpen(this)
|
||||||
|
|
||||||
this.ready = true
|
// Everything is now connected.
|
||||||
this.connecting = false
|
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
||||||
// Do this after we set the connection is ready to avoid errors when
|
|
||||||
// we try to send messages before the connection is ready.
|
|
||||||
this.onEngineConnectionOpen(this)
|
this.onEngineConnectionOpen(this)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||||
|
console.log(event)
|
||||||
console.log('unreliable data channel closed')
|
console.log('unreliable data channel closed')
|
||||||
this.close()
|
this.disconnectAll()
|
||||||
|
this.unreliableDataChannel = undefined
|
||||||
|
|
||||||
|
if (this.areAllConnectionsClosed()) {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: { type: DisconnectedType.Quit },
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('error', (event) => {
|
this.unreliableDataChannel.addEventListener('error', (event) => {
|
||||||
console.log('unreliable data channel error')
|
this.disconnectAll()
|
||||||
this.close()
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: {
|
||||||
|
type: DisconnectedType.Error,
|
||||||
|
value: new Error(event.toString()),
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.WebSocketConnecting,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websocket = new WebSocket(this.url, [])
|
||||||
|
this.websocket.binaryType = 'arraybuffer'
|
||||||
|
|
||||||
|
this.websocket.addEventListener('open', (event) => {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.WebSocketEstablished,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onWebsocketOpen(this)
|
||||||
|
|
||||||
|
// This is required for when KCMA is running stand-alone / within Tauri.
|
||||||
|
// Otherwise when run in a browser, the token is sent implicitly via
|
||||||
|
// the Cookie header.
|
||||||
|
if (this.token) {
|
||||||
|
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldTrace()) {
|
||||||
|
websocketSpan.resolve?.()
|
||||||
|
|
||||||
|
handshakeSpan = spanStart('handshake')
|
||||||
|
iceSpan = spanStart('ice')
|
||||||
|
dataChannelSpan = spanStart('data-channel')
|
||||||
|
mediaTrackSpan = spanStart('media-track')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldTrace()) {
|
||||||
|
void Promise.all([
|
||||||
|
handshakeSpan.promise,
|
||||||
|
iceSpan.promise,
|
||||||
|
dataChannelSpan.promise,
|
||||||
|
mediaTrackSpan.promise,
|
||||||
|
]).then(() => {
|
||||||
|
console.log('All spans finished, reporting')
|
||||||
|
webrtcMediaTransaction?.finish()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.websocket.addEventListener('close', (event) => {
|
||||||
|
this.disconnectAll()
|
||||||
|
this.websocket = undefined
|
||||||
|
|
||||||
|
if (this.areAllConnectionsClosed()) {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: { type: DisconnectedType.Quit },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.websocket.addEventListener('error', (event) => {
|
||||||
|
this.disconnectAll()
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: {
|
||||||
|
type: DisconnectedType.Error,
|
||||||
|
value: new Error(event.toString()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.websocket.addEventListener('message', (event) => {
|
||||||
|
// In the EngineConnection, we're looking for messages to/from
|
||||||
|
// the server that relate to the ICE handshake, or WebRTC
|
||||||
|
// negotiation. There may be other messages (including ArrayBuffer
|
||||||
|
// messages) that are intended for the GUI itself, so be careful
|
||||||
|
// when assuming we're the only consumer or that all messages will
|
||||||
|
// be carefully formatted here.
|
||||||
|
|
||||||
|
if (typeof event.data !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||||
|
|
||||||
|
if (!message.success) {
|
||||||
|
const errorsString = message?.errors
|
||||||
|
?.map((error) => {
|
||||||
|
return ` - ${error.error_code}: ${error.message}`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
if (message.request_id) {
|
||||||
|
console.error(
|
||||||
|
`Error in response to request ${message.request_id}:\n${errorsString}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.error(`Error from server:\n${errorsString}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = message.resp
|
||||||
|
|
||||||
|
// If there's no body to the response, we can bail here.
|
||||||
|
// !resp.type is usually "pong" response for our "ping"
|
||||||
|
if (!resp || !resp.type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('received', resp)
|
||||||
|
|
||||||
|
switch (resp.type) {
|
||||||
|
case 'ice_server_info':
|
||||||
|
let ice_servers = resp.data?.ice_servers
|
||||||
|
|
||||||
|
// Now that we have some ICE servers it makes sense
|
||||||
|
// to start initializing the RTCPeerConnection. RTCPeerConnection
|
||||||
|
// will begin the ICE process.
|
||||||
|
createPeerConnection()
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.PeerConnectionCreated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// No ICE servers can be valid in a local dev. env.
|
||||||
|
if (ice_servers?.length === 0) {
|
||||||
|
console.warn('No ICE servers')
|
||||||
|
this.pc?.setConfiguration({})
|
||||||
|
} else {
|
||||||
|
// When we set the Configuration, we want to always force
|
||||||
|
// iceTransportPolicy to 'relay', since we know the topology
|
||||||
|
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
||||||
|
// talk to the engine in any configuration /other/ than relay
|
||||||
|
// from a infra POV.
|
||||||
|
this.pc?.setConfiguration({
|
||||||
|
iceServers: ice_servers,
|
||||||
|
iceTransportPolicy: 'relay',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.ICEServersSet,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have an ICE Servers set now. We just setConfiguration, so let's
|
||||||
|
// start adding things we care about to the PeerConnection and let
|
||||||
|
// ICE negotiation happen in the background. Everything from here
|
||||||
|
// until the end of this function is setup of our end of the
|
||||||
|
// PeerConnection and waiting for events to fire our callbacks.
|
||||||
|
|
||||||
|
// Add a transceiver to our SDP offer
|
||||||
|
this.pc?.addTransceiver('video', {
|
||||||
|
direction: 'recvonly',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a session description offer based on our local environment
|
||||||
|
// that we will send to the remote end. The remote will send back
|
||||||
|
// what it supports via sdp_answer.
|
||||||
|
this.pc
|
||||||
|
?.createOffer()
|
||||||
|
.then((offer: RTCSessionDescriptionInit) => {
|
||||||
|
console.log(offer)
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.SetLocalDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return this.pc?.setLocalDescription(offer).then(() => {
|
||||||
|
this.send({
|
||||||
|
type: 'sdp_offer',
|
||||||
|
offer,
|
||||||
|
})
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.OfferedSdp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
console.error(error)
|
||||||
|
// The local description is invalid, so there's no point continuing.
|
||||||
|
this.disconnectAll()
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: {
|
||||||
|
type: DisconnectedType.Error,
|
||||||
|
value: error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'sdp_answer':
|
||||||
|
let answer = resp.data?.answer
|
||||||
|
if (!answer || answer.type === 'unspecified') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.ReceivedSdp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// As soon as this is set, RTCPeerConnection tries to
|
||||||
|
// establish a connection.
|
||||||
|
// @ts-ignore
|
||||||
|
// Have to ignore because dom.ts doesn't have the right type
|
||||||
|
void this.pc?.setRemoteDescription(answer)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.SetRemoteDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.WebRTCConnecting,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldTrace()) {
|
||||||
|
// When both ends have a local and remote SDP, we've been able to
|
||||||
|
// set up successfully. We'll still need to find the right ICE
|
||||||
|
// servers, but this is hand-shook.
|
||||||
|
handshakeSpan.resolve?.()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'trickle_ice':
|
||||||
|
let candidate = resp.data?.candidate
|
||||||
|
console.log('trickle_ice: using this candidate: ', candidate)
|
||||||
|
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'metrics_request':
|
||||||
|
if (this.webrtcStatsCollector === undefined) {
|
||||||
|
// TODO: Error message here?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void this.webrtcStatsCollector().then((client_metrics) => {
|
||||||
|
this.send({
|
||||||
|
type: 'metrics_response',
|
||||||
|
metrics: client_metrics,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
|
|
||||||
if (this.failedConnTimeout) {
|
if (this.failedConnTimeout) {
|
||||||
console.log('clearing timeout before set')
|
|
||||||
clearTimeout(this.failedConnTimeout)
|
clearTimeout(this.failedConnTimeout)
|
||||||
this.failedConnTimeout = null
|
this.failedConnTimeout = null
|
||||||
}
|
}
|
||||||
console.log('timeout set')
|
|
||||||
this.failedConnTimeout = setTimeout(() => {
|
this.failedConnTimeout = setTimeout(() => {
|
||||||
if (this.isReady()) {
|
if (this.isReady()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('engine connection timeout on connection, closing')
|
this.failedConnTimeout = null
|
||||||
this.close()
|
this.disconnectAll()
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
value: {
|
||||||
|
type: DisconnectedType.Timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
}, connectionTimeoutMs)
|
}, connectionTimeoutMs)
|
||||||
|
|
||||||
this.onConnectionStarted(this)
|
this.onConnectionStarted(this)
|
||||||
@ -549,23 +807,15 @@ class EngineConnection {
|
|||||||
typeof message === 'string' ? message : JSON.stringify(message)
|
typeof message === 'string' ? message : JSON.stringify(message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
close() {
|
disconnectAll() {
|
||||||
this.websocket?.close()
|
this.websocket?.close()
|
||||||
this.pc?.close()
|
|
||||||
this.unreliableDataChannel?.close()
|
this.unreliableDataChannel?.close()
|
||||||
this.websocket = undefined
|
this.pc?.close()
|
||||||
this.pc = undefined
|
|
||||||
this.unreliableDataChannel = undefined
|
|
||||||
this.webrtcStatsCollector = undefined
|
this.webrtcStatsCollector = undefined
|
||||||
if (this.failedConnTimeout) {
|
|
||||||
console.log('closed timeout in close')
|
|
||||||
clearTimeout(this.failedConnTimeout)
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
}
|
}
|
||||||
|
areAllConnectionsClosed() {
|
||||||
this.onClose(this)
|
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
||||||
this.ready = false
|
return !this.websocket && !this.pc && !this.unreliableDataChannel
|
||||||
this.connecting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,7 +935,7 @@ export class EngineCommandManager {
|
|||||||
// We also do this here because we want to ensure we create the gizmo
|
// We also do this here because we want to ensure we create the gizmo
|
||||||
// and execute the code everytime the stream is restarted.
|
// and execute the code everytime the stream is restarted.
|
||||||
const gizmoId = uuidv4()
|
const gizmoId = uuidv4()
|
||||||
this.sendSceneCommand({
|
void this.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: gizmoId,
|
cmd_id: gizmoId,
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -698,7 +948,7 @@ export class EngineCommandManager {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Initialize the planes.
|
// Initialize the planes.
|
||||||
this.initPlanes().then(() => {
|
void this.initPlanes().then(() => {
|
||||||
// We execute the code here to make sure if the stream was to
|
// We execute the code here to make sure if the stream was to
|
||||||
// restart in a session, we want to make sure to execute the code.
|
// restart in a session, we want to make sure to execute the code.
|
||||||
// We force it to re-execute the code because we want to make sure
|
// We force it to re-execute the code because we want to make sure
|
||||||
@ -745,7 +995,7 @@ export class EngineCommandManager {
|
|||||||
// because in all other cases we send JSON strings. But in the case of
|
// because in all other cases we send JSON strings. But in the case of
|
||||||
// export we send a binary blob.
|
// export we send a binary blob.
|
||||||
// Pass this to our export function.
|
// Pass this to our export function.
|
||||||
exportSave(event.data)
|
void exportSave(event.data)
|
||||||
} else {
|
} else {
|
||||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||||
event.data
|
event.data
|
||||||
|
@ -29,7 +29,7 @@ export default function CodeEditor() {
|
|||||||
The left pane is where you write your code. It's a code editor with
|
The left pane is where you write your code. It's a code editor with
|
||||||
syntax highlighting and autocompletion. We've decided to take the
|
syntax highlighting and autocompletion. We've decided to take the
|
||||||
difficult route of writing our own language—called <code>kcl</code>
|
difficult route of writing our own language—called <code>kcl</code>
|
||||||
—for describing geometry, because don't want to inherit all the
|
—for describing geometry, because we don't want to inherit all the
|
||||||
other functionality from existing languages. We have a lot of ideas
|
other functionality from existing languages. We have a lot of ideas
|
||||||
about how <code>kcl</code> will evolve, and we want to hear your
|
about how <code>kcl</code> will evolve, and we want to hear your
|
||||||
thoughts on it.
|
thoughts on it.
|
||||||
|
1
src/wasm-lib/Cargo.lock
generated
1
src/wasm-lib/Cargo.lock
generated
@ -1864,6 +1864,7 @@ dependencies = [
|
|||||||
"dashmap",
|
"dashmap",
|
||||||
"databake",
|
"databake",
|
||||||
"derive-docs 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"derive-docs 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"either",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
"futures",
|
"futures",
|
||||||
"insta",
|
"insta",
|
||||||
|
56
src/wasm-lib/grackle/src/error.rs
Normal file
56
src/wasm-lib/grackle/src/error.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use kcl_lib::ast::types::RequiredParamAfterOptionalParam;
|
||||||
|
use kittycad_execution_plan::ExecutionError;
|
||||||
|
|
||||||
|
use crate::String2;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
|
||||||
|
pub enum CompileError {
|
||||||
|
#[error("the name {name} was not defined")]
|
||||||
|
Undefined { name: String },
|
||||||
|
#[error("the function {fn_name} requires at least {required} arguments but you only supplied {actual}")]
|
||||||
|
NotEnoughArgs {
|
||||||
|
fn_name: String2,
|
||||||
|
required: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
#[error("the function {fn_name} accepts at most {maximum} arguments but you supplied {actual}")]
|
||||||
|
TooManyArgs {
|
||||||
|
fn_name: String2,
|
||||||
|
maximum: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
#[error("you tried to call {name} but it's not a function")]
|
||||||
|
NotCallable { name: String },
|
||||||
|
#[error("you're trying to use an operand that isn't compatible with the given arithmetic operator: {0}")]
|
||||||
|
InvalidOperand(&'static str),
|
||||||
|
#[error("you cannot use the value {0} as an index")]
|
||||||
|
InvalidIndex(String),
|
||||||
|
#[error("you tried to index into a value that isn't an array. Only arrays have numeric indices!")]
|
||||||
|
CannotIndex,
|
||||||
|
#[error("you tried to get the element {i} but that index is out of bounds. The array only has a length of {len}")]
|
||||||
|
IndexOutOfBounds { i: usize, len: usize },
|
||||||
|
#[error("you tried to access the property of a value that doesn't have any properties")]
|
||||||
|
NoProperties,
|
||||||
|
#[error("you tried to access a property of an array, but arrays don't have properties. They do have numeric indexes though, try using an index e.g. [0]")]
|
||||||
|
ArrayDoesNotHaveProperties,
|
||||||
|
#[error(
|
||||||
|
"you tried to read the '.{property}' of an object, but the object doesn't have any properties with that key"
|
||||||
|
)]
|
||||||
|
UndefinedProperty { property: String },
|
||||||
|
#[error("{0}")]
|
||||||
|
BadParamOrder(RequiredParamAfterOptionalParam),
|
||||||
|
#[error("A KCL function cannot have anything after its return value")]
|
||||||
|
MultipleReturns,
|
||||||
|
#[error("A KCL function must end with a return statement, but your function doesn't have one.")]
|
||||||
|
NoReturnStmt,
|
||||||
|
#[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")]
|
||||||
|
NotInPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("{0}")]
|
||||||
|
Compile(#[from] CompileError),
|
||||||
|
#[error("{0}")]
|
||||||
|
Execution(#[from] ExecutionError),
|
||||||
|
}
|
@ -9,6 +9,7 @@ pub enum KclValueGroup {
|
|||||||
ObjectExpression(Box<ast::types::ObjectExpression>),
|
ObjectExpression(Box<ast::types::ObjectExpression>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum SingleValue {
|
pub enum SingleValue {
|
||||||
Literal(Box<ast::types::Literal>),
|
Literal(Box<ast::types::Literal>),
|
||||||
Identifier(Box<ast::types::Identifier>),
|
Identifier(Box<ast::types::Identifier>),
|
||||||
@ -19,6 +20,7 @@ pub enum SingleValue {
|
|||||||
KclNoneExpression(ast::types::KclNone),
|
KclNoneExpression(ast::types::KclNone),
|
||||||
MemberExpression(Box<ast::types::MemberExpression>),
|
MemberExpression(Box<ast::types::MemberExpression>),
|
||||||
FunctionExpression(Box<ast::types::FunctionExpression>),
|
FunctionExpression(Box<ast::types::FunctionExpression>),
|
||||||
|
PipeSubstitution(Box<ast::types::PipeSubstitution>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ast::types::BinaryPart> for KclValueGroup {
|
impl From<ast::types::BinaryPart> for KclValueGroup {
|
||||||
@ -61,7 +63,7 @@ impl From<ast::types::Value> for KclValueGroup {
|
|||||||
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
|
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
|
||||||
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
|
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
|
||||||
ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
|
ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
|
||||||
ast::types::Value::PipeSubstitution(_) => todo!(),
|
ast::types::Value::PipeSubstitution(e) => Self::Single(SingleValue::PipeSubstitution(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,6 +81,7 @@ impl From<KclValueGroup> for ast::types::Value {
|
|||||||
SingleValue::KclNoneExpression(e) => ast::types::Value::None(e),
|
SingleValue::KclNoneExpression(e) => ast::types::Value::None(e),
|
||||||
SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e),
|
SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e),
|
||||||
SingleValue::FunctionExpression(e) => ast::types::Value::FunctionExpression(e),
|
SingleValue::FunctionExpression(e) => ast::types::Value::FunctionExpression(e),
|
||||||
|
SingleValue::PipeSubstitution(e) => ast::types::Value::PipeSubstitution(e),
|
||||||
},
|
},
|
||||||
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
|
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
|
||||||
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
|
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
mod binding_scope;
|
mod binding_scope;
|
||||||
|
mod error;
|
||||||
mod kcl_value_group;
|
mod kcl_value_group;
|
||||||
mod native_functions;
|
mod native_functions;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -8,15 +9,16 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use kcl_lib::{
|
use kcl_lib::{
|
||||||
ast,
|
ast,
|
||||||
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program, RequiredParamAfterOptionalParam},
|
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
|
||||||
};
|
};
|
||||||
use kittycad_execution_plan as ep;
|
use kittycad_execution_plan as ep;
|
||||||
use kittycad_execution_plan::{Address, ExecutionError, Instruction};
|
use kittycad_execution_plan::{Address, Instruction};
|
||||||
use kittycad_execution_plan_traits as ept;
|
use kittycad_execution_plan_traits as ept;
|
||||||
use kittycad_execution_plan_traits::NumericPrimitive;
|
use kittycad_execution_plan_traits::NumericPrimitive;
|
||||||
use kittycad_modeling_session::Session;
|
use kittycad_modeling_session::Session;
|
||||||
|
|
||||||
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
|
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
|
||||||
|
use self::error::{CompileError, Error};
|
||||||
use self::kcl_value_group::{KclValueGroup, SingleValue};
|
use self::kcl_value_group::{KclValueGroup, SingleValue};
|
||||||
|
|
||||||
/// Execute a KCL program by compiling into an execution plan, then running that.
|
/// Execute a KCL program by compiling into an execution plan, then running that.
|
||||||
@ -54,16 +56,17 @@ impl Planner {
|
|||||||
if retval.is_some() {
|
if retval.is_some() {
|
||||||
return Err(CompileError::MultipleReturns);
|
return Err(CompileError::MultipleReturns);
|
||||||
}
|
}
|
||||||
|
let mut ctx = Context::default();
|
||||||
let instructions_for_this_node = match item {
|
let instructions_for_this_node = match item {
|
||||||
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
|
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
|
||||||
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?.instructions,
|
KclValueGroup::Single(value) => self.plan_to_compute_single(&mut ctx, value)?.instructions,
|
||||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||||
},
|
},
|
||||||
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
|
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
|
||||||
BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) {
|
BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) {
|
||||||
KclValueGroup::Single(value) => {
|
KclValueGroup::Single(value) => {
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
|
let EvalPlan { instructions, binding } = self.plan_to_compute_single(&mut ctx, value)?;
|
||||||
retval = Some(binding);
|
retval = Some(binding);
|
||||||
instructions
|
instructions
|
||||||
}
|
}
|
||||||
@ -78,7 +81,7 @@ impl Planner {
|
|||||||
|
|
||||||
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
|
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
|
||||||
/// Returns the instructions, and the destination address of the value.
|
/// Returns the instructions, and the destination address of the value.
|
||||||
fn plan_to_compute_single(&mut self, value: SingleValue) -> Result<EvalPlan, CompileError> {
|
fn plan_to_compute_single(&mut self, ctx: &mut Context, value: SingleValue) -> Result<EvalPlan, CompileError> {
|
||||||
match value {
|
match value {
|
||||||
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
|
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
|
||||||
let address = self.next_addr.offset_by(1);
|
let address = self.next_addr.offset_by(1);
|
||||||
@ -121,7 +124,27 @@ impl Planner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
SingleValue::Identifier(expr) => {
|
SingleValue::Identifier(expr) => {
|
||||||
// This is just duplicating a binding.
|
// The KCL parser interprets bools as identifiers.
|
||||||
|
// Consider changing them to be KCL literals instead.
|
||||||
|
let b = if expr.name == "true" {
|
||||||
|
Some(true)
|
||||||
|
} else if expr.name == "false" {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(b) = b {
|
||||||
|
let address = self.next_addr.offset_by(1);
|
||||||
|
return Ok(EvalPlan {
|
||||||
|
instructions: vec![Instruction::SetPrimitive {
|
||||||
|
address,
|
||||||
|
value: ept::Primitive::Bool(b),
|
||||||
|
}],
|
||||||
|
binding: EpBinding::Single(address),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This identifier is just duplicating a binding.
|
||||||
// So, don't emit any instructions, because the value has already been computed.
|
// So, don't emit any instructions, because the value has already been computed.
|
||||||
// Just return the address that it was stored at after being computed.
|
// Just return the address that it was stored at after being computed.
|
||||||
let previously_bound_to = self
|
let previously_bound_to = self
|
||||||
@ -134,7 +157,7 @@ impl Planner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
SingleValue::UnaryExpression(expr) => {
|
SingleValue::UnaryExpression(expr) => {
|
||||||
let operand = self.plan_to_compute_single(SingleValue::from(expr.argument))?;
|
let operand = self.plan_to_compute_single(ctx, SingleValue::from(expr.argument))?;
|
||||||
let EpBinding::Single(binding) = operand.binding else {
|
let EpBinding::Single(binding) = operand.binding else {
|
||||||
return Err(CompileError::InvalidOperand(
|
return Err(CompileError::InvalidOperand(
|
||||||
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
||||||
@ -158,8 +181,8 @@ impl Planner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
SingleValue::BinaryExpression(expr) => {
|
SingleValue::BinaryExpression(expr) => {
|
||||||
let l = self.plan_to_compute_single(SingleValue::from(expr.left))?;
|
let l = self.plan_to_compute_single(ctx, SingleValue::from(expr.left))?;
|
||||||
let r = self.plan_to_compute_single(SingleValue::from(expr.right))?;
|
let r = self.plan_to_compute_single(ctx, SingleValue::from(expr.right))?;
|
||||||
let EpBinding::Single(l_binding) = l.binding else {
|
let EpBinding::Single(l_binding) = l.binding else {
|
||||||
return Err(CompileError::InvalidOperand(
|
return Err(CompileError::InvalidOperand(
|
||||||
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
||||||
@ -207,7 +230,7 @@ impl Planner {
|
|||||||
instructions: new_instructions,
|
instructions: new_instructions,
|
||||||
binding: arg,
|
binding: arg,
|
||||||
} = match KclValueGroup::from(argument) {
|
} = match KclValueGroup::from(argument) {
|
||||||
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?,
|
KclValueGroup::Single(value) => self.plan_to_compute_single(ctx, value)?,
|
||||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||||
};
|
};
|
||||||
@ -323,7 +346,56 @@ impl Planner {
|
|||||||
binding: binding.clone(),
|
binding: binding.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SingleValue::PipeExpression(_) => todo!(),
|
SingleValue::PipeSubstitution(_expr) => {
|
||||||
|
if let Some(ref binding) = ctx.pipe_substitution {
|
||||||
|
Ok(EvalPlan {
|
||||||
|
instructions: Vec::new(),
|
||||||
|
binding: binding.clone(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(CompileError::NotInPipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SingleValue::PipeExpression(expr) => {
|
||||||
|
let mut bodies = expr.body.into_iter();
|
||||||
|
|
||||||
|
// Get the first expression (i.e. body) of the pipeline.
|
||||||
|
let first = bodies.next().expect("Pipe expression must have > 1 item");
|
||||||
|
let EvalPlan {
|
||||||
|
mut instructions,
|
||||||
|
binding: mut current_value,
|
||||||
|
} = match KclValueGroup::from(first) {
|
||||||
|
KclValueGroup::Single(v) => self.plan_to_compute_single(ctx, v)?,
|
||||||
|
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||||
|
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the remaining bodies.
|
||||||
|
for body in bodies {
|
||||||
|
let value = match KclValueGroup::from(body) {
|
||||||
|
KclValueGroup::Single(v) => v,
|
||||||
|
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||||
|
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||||
|
};
|
||||||
|
// This body will probably contain a % (pipe substitution character).
|
||||||
|
// So it needs to know what the previous pipeline body's value is,
|
||||||
|
// to replace the % with that value.
|
||||||
|
ctx.pipe_substitution = Some(current_value.clone());
|
||||||
|
let EvalPlan {
|
||||||
|
instructions: instructions_for_this_body,
|
||||||
|
binding,
|
||||||
|
} = self.plan_to_compute_single(ctx, value)?;
|
||||||
|
instructions.extend(instructions_for_this_body);
|
||||||
|
current_value = binding;
|
||||||
|
}
|
||||||
|
// Before we return, clear the pipe substitution, because nothing outside this
|
||||||
|
// pipeline should be able to use it anymore.
|
||||||
|
ctx.pipe_substitution = None;
|
||||||
|
Ok(EvalPlan {
|
||||||
|
instructions,
|
||||||
|
binding: current_value,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,11 +406,12 @@ impl Planner {
|
|||||||
&mut self,
|
&mut self,
|
||||||
declarations: ast::types::VariableDeclaration,
|
declarations: ast::types::VariableDeclaration,
|
||||||
) -> Result<Vec<Instruction>, CompileError> {
|
) -> Result<Vec<Instruction>, CompileError> {
|
||||||
|
let mut ctx = Context::default();
|
||||||
declarations
|
declarations
|
||||||
.declarations
|
.declarations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(Vec::new(), |mut acc, declaration| {
|
.try_fold(Vec::new(), |mut acc, declaration| {
|
||||||
let (instrs, binding) = self.plan_to_bind_one(declaration.init)?;
|
let (instrs, binding) = self.plan_to_bind_one(&mut ctx, declaration.init)?;
|
||||||
self.binding_scope.bind(declaration.id.name, binding);
|
self.binding_scope.bind(declaration.id.name, binding);
|
||||||
acc.extend(instrs);
|
acc.extend(instrs);
|
||||||
Ok(acc)
|
Ok(acc)
|
||||||
@ -347,13 +420,14 @@ impl Planner {
|
|||||||
|
|
||||||
fn plan_to_bind_one(
|
fn plan_to_bind_one(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
ctx: &mut Context,
|
||||||
value_being_bound: ast::types::Value,
|
value_being_bound: ast::types::Value,
|
||||||
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
|
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
|
||||||
match KclValueGroup::from(value_being_bound) {
|
match KclValueGroup::from(value_being_bound) {
|
||||||
KclValueGroup::Single(init_value) => {
|
KclValueGroup::Single(init_value) => {
|
||||||
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
|
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
|
||||||
// and bind it to the KCL identifier.
|
// and bind it to the KCL identifier.
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(init_value)?;
|
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, init_value)?;
|
||||||
Ok((instructions, binding))
|
Ok((instructions, binding))
|
||||||
}
|
}
|
||||||
KclValueGroup::ArrayExpression(expr) => {
|
KclValueGroup::ArrayExpression(expr) => {
|
||||||
@ -366,7 +440,7 @@ impl Planner {
|
|||||||
KclValueGroup::Single(value) => {
|
KclValueGroup::Single(value) => {
|
||||||
// If this element of the array is a single value, then binding it is
|
// If this element of the array is a single value, then binding it is
|
||||||
// straightforward -- you got a single binding, no need to change anything.
|
// straightforward -- you got a single binding, no need to change anything.
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
|
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
acc_bindings.push(binding);
|
acc_bindings.push(binding);
|
||||||
}
|
}
|
||||||
@ -379,7 +453,7 @@ impl Planner {
|
|||||||
.elements
|
.elements
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(Vec::new(), |mut seq, child_element| {
|
.try_fold(Vec::new(), |mut seq, child_element| {
|
||||||
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
|
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
seq.push(binding);
|
seq.push(binding);
|
||||||
Ok(seq)
|
Ok(seq)
|
||||||
@ -397,7 +471,7 @@ impl Planner {
|
|||||||
.properties
|
.properties
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(map, |mut map, property| {
|
.try_fold(map, |mut map, property| {
|
||||||
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
|
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
|
||||||
map.insert(property.key.name, binding);
|
map.insert(property.key.name, binding);
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@ -419,7 +493,7 @@ impl Planner {
|
|||||||
|(mut acc_instrs, mut acc_bindings), (key, value)| {
|
|(mut acc_instrs, mut acc_bindings), (key, value)| {
|
||||||
match KclValueGroup::from(value) {
|
match KclValueGroup::from(value) {
|
||||||
KclValueGroup::Single(value) => {
|
KclValueGroup::Single(value) => {
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
|
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
acc_bindings.insert(key.name, binding);
|
acc_bindings.insert(key.name, binding);
|
||||||
}
|
}
|
||||||
@ -432,7 +506,7 @@ impl Planner {
|
|||||||
.elements
|
.elements
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
|
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
|
||||||
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
|
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
|
||||||
seq.push(binding);
|
seq.push(binding);
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
Ok(seq)
|
Ok(seq)
|
||||||
@ -450,7 +524,7 @@ impl Planner {
|
|||||||
.properties
|
.properties
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(HashMap::with_capacity(n), |mut map, property| {
|
.try_fold(HashMap::with_capacity(n), |mut map, property| {
|
||||||
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
|
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
|
||||||
map.insert(property.key.name, binding);
|
map.insert(property.key.name, binding);
|
||||||
acc_instrs.extend(instructions);
|
acc_instrs.extend(instructions);
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@ -468,56 +542,6 @@ impl Planner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
|
|
||||||
pub enum CompileError {
|
|
||||||
#[error("the name {name} was not defined")]
|
|
||||||
Undefined { name: String },
|
|
||||||
#[error("the function {fn_name} requires at least {required} arguments but you only supplied {actual}")]
|
|
||||||
NotEnoughArgs {
|
|
||||||
fn_name: String2,
|
|
||||||
required: usize,
|
|
||||||
actual: usize,
|
|
||||||
},
|
|
||||||
#[error("the function {fn_name} accepts at most {maximum} arguments but you supplied {actual}")]
|
|
||||||
TooManyArgs {
|
|
||||||
fn_name: String2,
|
|
||||||
maximum: usize,
|
|
||||||
actual: usize,
|
|
||||||
},
|
|
||||||
#[error("you tried to call {name} but it's not a function")]
|
|
||||||
NotCallable { name: String },
|
|
||||||
#[error("you're trying to use an operand that isn't compatible with the given arithmetic operator: {0}")]
|
|
||||||
InvalidOperand(&'static str),
|
|
||||||
#[error("you cannot use the value {0} as an index")]
|
|
||||||
InvalidIndex(String),
|
|
||||||
#[error("you tried to index into a value that isn't an array. Only arrays have numeric indices!")]
|
|
||||||
CannotIndex,
|
|
||||||
#[error("you tried to get the element {i} but that index is out of bounds. The array only has a length of {len}")]
|
|
||||||
IndexOutOfBounds { i: usize, len: usize },
|
|
||||||
#[error("you tried to access the property of a value that doesn't have any properties")]
|
|
||||||
NoProperties,
|
|
||||||
#[error("you tried to access a property of an array, but arrays don't have properties. They do have numeric indexes though, try using an index e.g. [0]")]
|
|
||||||
ArrayDoesNotHaveProperties,
|
|
||||||
#[error(
|
|
||||||
"you tried to read the '.{property}' of an object, but the object doesn't have any properties with that key"
|
|
||||||
)]
|
|
||||||
UndefinedProperty { property: String },
|
|
||||||
#[error("{0}")]
|
|
||||||
BadParamOrder(RequiredParamAfterOptionalParam),
|
|
||||||
#[error("A KCL function cannot have anything after its return value")]
|
|
||||||
MultipleReturns,
|
|
||||||
#[error("A KCL function must end with a return statement, but your function doesn't have one.")]
|
|
||||||
NoReturnStmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("{0}")]
|
|
||||||
Compile(#[from] CompileError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Execution(#[from] ExecutionError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
|
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
|
||||||
/// bound to some KCL name and Execution Plan address.
|
/// bound to some KCL name and Execution Plan address.
|
||||||
fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
|
fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
|
||||||
@ -562,3 +586,9 @@ enum KclFunction {
|
|||||||
Add(native_functions::Add),
|
Add(native_functions::Add),
|
||||||
UserDefined(UserDefinedFunction),
|
UserDefined(UserDefinedFunction),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context used when compiling KCL.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Context {
|
||||||
|
pipe_substitution: Option<EpBinding>,
|
||||||
|
}
|
||||||
|
@ -128,6 +128,23 @@ fn name_not_found() {
|
|||||||
assert_eq!(err, CompileError::Undefined { name: "y".to_owned() });
|
assert_eq!(err, CompileError::Undefined { name: "y".to_owned() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_bool() {
|
||||||
|
// Check that Grackle properly compiles KCL bools to EP bools.
|
||||||
|
for (str, val) in [("true", true), ("false", false)] {
|
||||||
|
let program = format!("let x = {str}");
|
||||||
|
let (plan, scope) = must_plan(&program);
|
||||||
|
assert_eq!(
|
||||||
|
plan,
|
||||||
|
vec![Instruction::SetPrimitive {
|
||||||
|
address: Address::ZERO,
|
||||||
|
value: val.into(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(scope.get("x"), Some(&EpBinding::Single(Address::ZERO)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aliases() {
|
fn aliases() {
|
||||||
let program = "
|
let program = "
|
||||||
@ -511,7 +528,6 @@ fn define_recursive_function() {
|
|||||||
let (plan, _scope) = must_plan(program);
|
let (plan, _scope) = must_plan(program);
|
||||||
assert_eq!(plan, Vec::new())
|
assert_eq!(plan, Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn use_kcl_function_as_param() {
|
fn use_kcl_function_as_param() {
|
||||||
let program = "fn wrapper = (f) => {
|
let program = "fn wrapper = (f) => {
|
||||||
@ -539,142 +555,6 @@ fn use_kcl_function_as_param() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_kcl_function_y_combinator() {
|
|
||||||
let program = "
|
|
||||||
// TRUE := λx.λy.x
|
|
||||||
fn _TRUE = (x) => {
|
|
||||||
return (y) => { return x }
|
|
||||||
}
|
|
||||||
|
|
||||||
// FALSE := λx.λy.y
|
|
||||||
fn _FALSE = (x) => {
|
|
||||||
return (y) => { return y }
|
|
||||||
}
|
|
||||||
|
|
||||||
// constant false (no matter what is applied, the falsey value is returned)
|
|
||||||
fn cFalse = (x) => {
|
|
||||||
return _FALSE
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISZERO := λn.n (λx.FALSE) TRUE
|
|
||||||
fn is_zero = (n) => {
|
|
||||||
let fa = n(cFalse)
|
|
||||||
return fa(_TRUE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IFTHENELSE := λp.λa.λb.p a b
|
|
||||||
fn ifthenelse = (p) => {
|
|
||||||
return (a) => {
|
|
||||||
return (b) => {
|
|
||||||
let fa = p(a)
|
|
||||||
return fa(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SUCC := λn.λf.λx.f (n f x)
|
|
||||||
// Inserts another (f x) in the church numeral chain
|
|
||||||
fn succ = (n) => {
|
|
||||||
return (f) => {
|
|
||||||
return (x) => {
|
|
||||||
let fa = n(f)
|
|
||||||
let fb = fa(x)
|
|
||||||
return f(fb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PLUS := λm.λn.m SUCC n
|
|
||||||
fn plus = (m) => {
|
|
||||||
return (n) => {
|
|
||||||
let fa = m(succ)
|
|
||||||
return fa(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0 := λf.λx.x
|
|
||||||
fn _0 = (f) => {
|
|
||||||
return (x) => { return x }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cZero = (x) => {
|
|
||||||
return _0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 := λf.λx.f x
|
|
||||||
fn _1 = (f) => {
|
|
||||||
return (x) => { return f(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
let _2 = succ(_1)
|
|
||||||
let _3 = succ(_2)
|
|
||||||
let _4 = succ(_3)
|
|
||||||
let _5 = succ(_4)
|
|
||||||
let _6 = succ(_5)
|
|
||||||
// ...
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// PRED := λn.n (λg.λk.ISZERO (g 1) k (PLUS (g k) 1)) (λv.0) 0
|
|
||||||
fn pred = (n) => {
|
|
||||||
fn f1 = (g) => {
|
|
||||||
return (k) => {
|
|
||||||
let fa = is_zero(g(_1))
|
|
||||||
let fb = fa(k)
|
|
||||||
let fc1 = plus(g(k))
|
|
||||||
let fc2 = fc1(_1)
|
|
||||||
let fc = fb(fc2)
|
|
||||||
return fc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let f2 = n(f1)
|
|
||||||
let f3 = f2(cZero)
|
|
||||||
let f4 = f3(_0)
|
|
||||||
return f4
|
|
||||||
}
|
|
||||||
|
|
||||||
// MUL := λm.λn.m (PLUS n) 0
|
|
||||||
fn mul = (m) => {
|
|
||||||
return (n) => {
|
|
||||||
let fa = m(plus(n))
|
|
||||||
let fb = fa(_0)
|
|
||||||
return fb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// G := λr. λn.(1, if n = 0; else n × (r (n−1)))
|
|
||||||
fn G = (r) => {
|
|
||||||
return (n) => {
|
|
||||||
let fa = ifthenelse(n)
|
|
||||||
let fb = fa(_1)
|
|
||||||
let fc1 = mul(n)
|
|
||||||
let fc2 = fc1(r(pred(n)))
|
|
||||||
let fc = fb(fc2)
|
|
||||||
return fc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Y := λg.(λx.g (x x)) (λx.g (x x))
|
|
||||||
fn Y = (g) => {
|
|
||||||
fn f1 = (x) => { return g(x(x)) }
|
|
||||||
let f2 = g(f1)
|
|
||||||
let f3 = f2(f1)
|
|
||||||
return f3
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fact = (n) => {
|
|
||||||
let fa = Y(G)
|
|
||||||
return fa(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// x should be _6
|
|
||||||
let x = fact(_3)
|
|
||||||
";
|
|
||||||
|
|
||||||
let (plan, scope) = must_plan(program);
|
|
||||||
// Somehow check the result is the same as _6 definition
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn use_kcl_functions_with_params() {
|
fn use_kcl_functions_with_params() {
|
||||||
for (i, program) in [
|
for (i, program) in [
|
||||||
@ -721,6 +601,33 @@ fn use_kcl_functions_with_params() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pipe_substitution_outside_pipe_expression() {
|
||||||
|
let program = "let x = add(1, %)";
|
||||||
|
let err = should_not_compile(program);
|
||||||
|
assert!(matches!(err, CompileError::NotInPipeline));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsugar_pipe_expressions() {
|
||||||
|
// These two programs should be equivalent,
|
||||||
|
// because that's just the definition of the |> operator.
|
||||||
|
let program2 = "
|
||||||
|
fn double = (x) => { return x * 2 }
|
||||||
|
fn triple = (x) => { return x * 3 }
|
||||||
|
let x = 1 |> double(%) |> triple(%) // should be 6
|
||||||
|
";
|
||||||
|
let program1 = "
|
||||||
|
fn double = (x) => { return x * 2 }
|
||||||
|
fn triple = (x) => { return x * 3 }
|
||||||
|
let x = triple(double(1)) // should be 6
|
||||||
|
";
|
||||||
|
// So, check that they are.
|
||||||
|
let (plan1, _) = must_plan(program1);
|
||||||
|
let (plan2, _) = must_plan(program2);
|
||||||
|
assert_eq!(plan1, plan2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn define_kcl_functions() {
|
fn define_kcl_functions() {
|
||||||
let (plan, scope) = must_plan("fn triple = (x) => { return x * 3 }");
|
let (plan, scope) = must_plan("fn triple = (x) => { return x * 3 }");
|
||||||
|
@ -32,6 +32,7 @@ thiserror = "1.0.50"
|
|||||||
ts-rs = { version = "7", features = ["uuid-impl"] }
|
ts-rs = { version = "7", features = ["uuid-impl"] }
|
||||||
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
||||||
winnow = "0.5.18"
|
winnow = "0.5.18"
|
||||||
|
either = "1.6.1"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
js-sys = { version = "0.3.65" }
|
js-sys = { version = "0.3.65" }
|
||||||
|
@ -19,6 +19,7 @@ use crate::{
|
|||||||
parser::PIPE_OPERATOR,
|
parser::PIPE_OPERATOR,
|
||||||
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
|
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
|
||||||
};
|
};
|
||||||
|
use crate::executor::PathToNode;
|
||||||
|
|
||||||
mod literal_value;
|
mod literal_value;
|
||||||
mod none;
|
mod none;
|
||||||
@ -1433,6 +1434,7 @@ impl From<Literal> for MemoryItem {
|
|||||||
value: JValue::from(literal.value.clone()),
|
value: JValue::from(literal.value.clone()),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: literal.into(),
|
source_range: literal.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1444,6 +1446,7 @@ impl From<&Box<Literal>> for MemoryItem {
|
|||||||
value: JValue::from(literal.value.clone()),
|
value: JValue::from(literal.value.clone()),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: literal.into(),
|
source_range: literal.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1641,7 +1644,7 @@ impl ArrayExpression {
|
|||||||
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
|
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
|
||||||
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
|
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
|
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).await?,
|
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx, vec![]).await?,
|
||||||
Value::PipeSubstitution(pipe_substitution) => {
|
Value::PipeSubstitution(pipe_substitution) => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
|
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
|
||||||
@ -1665,6 +1668,7 @@ impl ArrayExpression {
|
|||||||
value: results.into(),
|
value: results.into(),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1794,7 +1798,7 @@ impl ObjectExpression {
|
|||||||
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
|
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
|
||||||
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
|
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
|
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).await?,
|
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx, vec![]).await?,
|
||||||
Value::PipeSubstitution(pipe_substitution) => {
|
Value::PipeSubstitution(pipe_substitution) => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
|
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
|
||||||
@ -1822,6 +1826,7 @@ impl ObjectExpression {
|
|||||||
value: object.into(),
|
value: object.into(),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2031,6 +2036,7 @@ impl MemberExpression {
|
|||||||
value: value.clone(),
|
value: value.clone(),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@ -2087,6 +2093,7 @@ impl MemberExpression {
|
|||||||
value: value.clone(),
|
value: value.clone(),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@ -2251,6 +2258,7 @@ impl BinaryExpression {
|
|||||||
value,
|
value,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -2272,6 +2280,7 @@ impl BinaryExpression {
|
|||||||
value,
|
value,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2435,6 +2444,7 @@ impl UnaryExpression {
|
|||||||
value: (-(num)).into(),
|
value: (-(num)).into(),
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
|
path_to_node: vec![],
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2564,11 +2574,12 @@ impl PipeExpression {
|
|||||||
memory: &mut ProgramMemory,
|
memory: &mut ProgramMemory,
|
||||||
pipe_info: &mut PipeInfo,
|
pipe_info: &mut PipeInfo,
|
||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
|
path_to_node: PathToNode,
|
||||||
) -> Result<MemoryItem, KclError> {
|
) -> Result<MemoryItem, KclError> {
|
||||||
// Reset the previous results.
|
// Reset the previous results.
|
||||||
pipe_info.previous_results = vec![];
|
pipe_info.previous_results = vec![];
|
||||||
pipe_info.index = 0;
|
pipe_info.index = 0;
|
||||||
execute_pipe_body(memory, &self.body, pipe_info, self.into(), ctx).await
|
execute_pipe_body(memory, &self.body, pipe_info, self.into(), ctx, path_to_node).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rename all identifiers that have the old name to the new given name.
|
/// Rename all identifiers that have the old name to the new given name.
|
||||||
@ -2586,6 +2597,8 @@ async fn execute_pipe_body(
|
|||||||
pipe_info: &mut PipeInfo,
|
pipe_info: &mut PipeInfo,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
|
path_to_node: PathToNode,
|
||||||
|
|
||||||
) -> Result<MemoryItem, KclError> {
|
) -> Result<MemoryItem, KclError> {
|
||||||
if pipe_info.index == body.len() {
|
if pipe_info.index == body.len() {
|
||||||
pipe_info.is_in_pipe = false;
|
pipe_info.is_in_pipe = false;
|
||||||
|
@ -11,6 +11,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JValue;
|
use serde_json::Value as JValue;
|
||||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
||||||
|
// use either::Either;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
|
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
|
||||||
@ -632,6 +633,18 @@ impl From<Point3d> for kittycad::types::Point3D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// number or string
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum NumberOrString {
|
||||||
|
Num(i32), // assuming 'number' is equivalent to a 32-bit integer
|
||||||
|
Str(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PathToNode
|
||||||
|
pub type PathToNode = Vec<(NumberOrString, String)>;
|
||||||
|
|
||||||
|
|
||||||
/// Metadata.
|
/// Metadata.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -639,11 +652,16 @@ impl From<Point3d> for kittycad::types::Point3D {
|
|||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
/// The source range.
|
/// The source range.
|
||||||
pub source_range: SourceRange,
|
pub source_range: SourceRange,
|
||||||
|
/// The path to node for this memory Item
|
||||||
|
pub path_to_node: PathToNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SourceRange> for Metadata {
|
impl From<SourceRange> for Metadata {
|
||||||
fn from(source_range: SourceRange) -> Self {
|
fn from(source_range: SourceRange) -> Self {
|
||||||
Self { source_range }
|
Self {
|
||||||
|
source_range,
|
||||||
|
path_to_node: Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -829,12 +847,17 @@ pub async fn execute(
|
|||||||
) -> Result<ProgramMemory, KclError> {
|
) -> Result<ProgramMemory, KclError> {
|
||||||
let mut pipe_info = PipeInfo::default();
|
let mut pipe_info = PipeInfo::default();
|
||||||
|
|
||||||
|
// let path_to_Node: PathToNode = vec![("body".to_string(), "".to_string())];
|
||||||
|
let path_to_node: PathToNode = vec![(NumberOrString::Str("body".to_string()), "".to_string())];
|
||||||
|
|
||||||
// Iterate over the body of the program.
|
// Iterate over the body of the program.
|
||||||
for statement in &program.body {
|
for (index, statement) in program.body.iter().enumerate() {
|
||||||
|
let mut with_body_path_to_node = path_to_node.clone();
|
||||||
|
with_body_path_to_node.push((NumberOrString::Num(index as i32), "index".to_string()));
|
||||||
match statement {
|
match statement {
|
||||||
BodyItem::ExpressionStatement(expression_statement) => {
|
BodyItem::ExpressionStatement(expression_statement) => {
|
||||||
if let Value::PipeExpression(pipe_expr) = &expression_statement.expression {
|
if let Value::PipeExpression(pipe_expr) = &expression_statement.expression {
|
||||||
pipe_expr.get_result(memory, &mut pipe_info, ctx).await?;
|
pipe_expr.get_result(memory, &mut pipe_info, ctx, with_body_path_to_node).await?;
|
||||||
} else if let Value::CallExpression(call_expr) = &expression_statement.expression {
|
} else if let Value::CallExpression(call_expr) = &expression_statement.expression {
|
||||||
let fn_name = call_expr.callee.name.to_string();
|
let fn_name = call_expr.callee.name.to_string();
|
||||||
let mut args: Vec<MemoryItem> = Vec::new();
|
let mut args: Vec<MemoryItem> = Vec::new();
|
||||||
@ -905,10 +928,15 @@ pub async fn execute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BodyItem::VariableDeclaration(variable_declaration) => {
|
BodyItem::VariableDeclaration(variable_declaration) => {
|
||||||
for declaration in &variable_declaration.declarations {
|
|
||||||
|
for (index, declaration) in variable_declaration.declarations.iter().enumerate() {
|
||||||
let var_name = declaration.id.name.to_string();
|
let var_name = declaration.id.name.to_string();
|
||||||
let source_range: SourceRange = declaration.init.clone().into();
|
let source_range: SourceRange = declaration.init.clone().into();
|
||||||
let metadata = Metadata { source_range };
|
let mut with_dec_path_to_node = with_body_path_to_node.clone();
|
||||||
|
with_dec_path_to_node.push((NumberOrString::Str("declarations".to_string()), "VariableDeclaration".to_string()));
|
||||||
|
with_dec_path_to_node.push((NumberOrString::Num(index as i32), "index".to_string()));
|
||||||
|
with_dec_path_to_node.push((NumberOrString::Str("init".to_string()), "".to_string()));
|
||||||
|
let metadata = Metadata { source_range, path_to_node: with_dec_path_to_node.clone() };
|
||||||
|
|
||||||
match &declaration.init {
|
match &declaration.init {
|
||||||
Value::None(none) => {
|
Value::None(none) => {
|
||||||
@ -963,7 +991,7 @@ pub async fn execute(
|
|||||||
memory.add(&var_name, result, source_range)?;
|
memory.add(&var_name, result, source_range)?;
|
||||||
}
|
}
|
||||||
Value::PipeExpression(pipe_expression) => {
|
Value::PipeExpression(pipe_expression) => {
|
||||||
let result = pipe_expression.get_result(memory, &mut pipe_info, ctx).await?;
|
let result = pipe_expression.get_result(memory, &mut pipe_info, ctx, with_dec_path_to_node).await?;
|
||||||
memory.add(&var_name, result, source_range)?;
|
memory.add(&var_name, result, source_range)?;
|
||||||
}
|
}
|
||||||
Value::PipeSubstitution(pipe_substitution) => {
|
Value::PipeSubstitution(pipe_substitution) => {
|
||||||
@ -1027,7 +1055,7 @@ pub async fn execute(
|
|||||||
memory.return_ = Some(ProgramReturn::Value(result));
|
memory.return_ = Some(ProgramReturn::Value(result));
|
||||||
}
|
}
|
||||||
Value::PipeExpression(pipe_expr) => {
|
Value::PipeExpression(pipe_expr) => {
|
||||||
let result = pipe_expr.get_result(memory, &mut pipe_info, ctx).await?;
|
let result = pipe_expr.get_result(memory, &mut pipe_info, ctx, with_body_path_to_node).await?;
|
||||||
memory.return_ = Some(ProgramReturn::Value(result));
|
memory.return_ = Some(ProgramReturn::Value(result));
|
||||||
}
|
}
|
||||||
Value::PipeSubstitution(_) => {}
|
Value::PipeSubstitution(_) => {}
|
||||||
|
@ -190,6 +190,7 @@ impl Args {
|
|||||||
value: j,
|
value: j,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.source_range,
|
source_range: self.source_range,
|
||||||
|
path_to_node: Vec::new()
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user