Compare commits

...

10 Commits

Author SHA1 Message Date
7a7a83c835 path to node progress 2024-01-25 09:53:14 +11:00
de63e4f19f Grackle: Refactor: Move error types into their own module (#1319)
Refactor: Move error types into their own submodule
2024-01-24 05:47:56 +00:00
b70b271e6b Grackle: compile KCL bools to EP bools (#1318) 2024-01-24 05:36:09 +00:00
08b7cdc5f6 Grackle: pipeline expressions (#1315)
Grackle can now compile |> pipelines. This means that these two programs compile to identical execution plans:

```kcl
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = 1 |> double(%) |> triple(%) // should be 6
```
```kcl
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = triple(double(1)) // should be 6
```

This required adding passing "what should % actually resolve to" through the program. This required modifying every call site of `plan_to_bind` and `plan_to_compute` to pass the data. To avoid doing this again, I wrapped that data into a struct called `Context` so that when we have more data like it, we can just add a new field and won't need to change every call site.
2024-01-24 10:05:40 +11:00
6efe6b54c0 Fix typo in onboarding (#1316)
fix typo
2024-01-23 17:46:34 -05:00
69f72d62e0 Rework initial engine connection logic (#1205) (#1221)
Rework EngineConnection class (#1205)

Co-authored-by: lf94 <inbox@leefallat.ca>
2024-01-23 13:13:43 -05:00
e04b09fcd8 Grackle: unary operations (#1308)
Support compiling logical not and sign-flipping negation.
2024-01-23 13:57:09 +11:00
4903f6b9fc Grackle: compile and execute user-defined KCL functions (#1306)
* Grackle: compile KCL function definitions

Definitions like `fn x = () => { return 1 }` can now be compiled. These functions can't be _called_ yet, but just defining them and mapping them to names works now.

* Failing test for executing a user-defined function

* Refactor: KclFunction is now an enum, not a trait

It's a pain in the ass to work with trait objects in Rust, so I'm refactoring to avoid needing traits at all. We can just use enums. This simplifies future work.

* Zero-parameter functions can be called

Finally, Grackle can actually run user-defined KCL functions! It basically treats them as a new, separate program (with its own scope of variables, nested within the existing parent scope).

* Failing test for multi-param KCL functions

* Execute user-defined functions which declare parameters

Previous commits in this PR got user-defined functions working, but only if they had zero parameters. In this commit, call arguments are bound to function parameters, so you can now compile functions with params.

* Users get a compile error if they try to pass more args to a function than it has parameters

This will help users get clear error messages.

* More test coverage

Among other things, this verify that Grackle compiles KCL functions which themselves either return or accept functions
2024-01-23 11:30:00 +11:00
ef8149f03a Bump vite from 4.5.1 to 4.5.2 (#1302)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 17:00:02 +11:00
1b75321bf1 Rust: Update h2 (#1304) 2024-01-21 23:54:04 +00:00
16 changed files with 1340 additions and 460 deletions

View File

@ -136,7 +136,7 @@
"prettier": "^2.8.0", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
"vite": "^4.5.1", "vite": "^4.5.2",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.1", "vite-tsconfig-paths": "^4.2.1",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",

6
src-tauri/Cargo.lock generated
View File

@ -1242,9 +1242,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.20" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -1252,7 +1252,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap 1.9.3", "indexmap 2.0.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",

View File

@ -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:
} this.send({ type: 'ping' })
if (this.isReady()) { break
// When we're online, every 10 seconds, we'll attempt to put a 'ping' case EngineConnectionStateType.Disconnected:
// command through the WebSocket connection. This will help both ends clearInterval(pingInterval)
// of the connection maintain the TCP connection without hitting a break
// timeout condition. default:
this.send({ type: 'ping' }) 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)
break
case DisconnectedType.Timeout:
console.log('Trying to reconnect')
this.connect()
break
default:
break
} }
if (this.isReady()) {
return
}
console.log('connecting via retry')
this.connect()
}, 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,71 +282,269 @@ 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')
}
const createPeerConnection = () => {
this.pc = new RTCPeerConnection()
// Data channels MUST BE specified before SDP offers because requesting
// them affects what our needs are!
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
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(),
})
}) })
websocketSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'websocket' }) this.pc.addEventListener('icecandidateerror', (_event: Event) => {
) const event = _event as RTCPeerConnectionIceErrorEvent
console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
})
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// 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()) {
iceSpan.resolve?.()
}
// Let the browser attach to the video stream now
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
break
case 'failed':
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: new Error(
'failed to negotiate ice connection; restarting'
),
},
}
break
default:
break
}
})
this.pc.addEventListener('track', (event) => {
const mediaStream = event.streams[0]
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.TrackReceived,
},
}
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) {
reject(new Error('too many video tracks to report'))
return
}
let videoTrack = mediaStream.getVideoTracks()[0]
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
rtc_frames_received: 0,
rtc_frames_per_second: 0,
rtc_freeze_count: 0,
rtc_jitter_sec: 0.0,
rtc_keyframes_decoded: 0,
rtc_total_freezes_duration_sec: 0.0,
}
// TODO(paultag): Since we can technically have multiple WebRTC
// video tracks (even if the Server doesn't at the moment), we
// ought to send stats for every video track(?), and add the stream
// ID into it. This raises the cardinality of collected metrics
// when/if we do, but for now, just report the one stream.
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
client_metrics.rtc_frames_decoded =
videoTrackReport.framesDecoded || 0
client_metrics.rtc_frames_dropped =
videoTrackReport.framesDropped || 0
client_metrics.rtc_frames_received =
videoTrackReport.framesReceived || 0
client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0
client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded || 0
client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent,
}
})
resolve(client_metrics)
})
})
}
// The app is eager to use the MediaStream; as soon as onNewTrack is
// called, the following sequence happens:
// 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.unreliableDataChannel = event.channel
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.DataChannelConnecting,
value: event.channel.label,
},
}
this.unreliableDataChannel.addEventListener('open', (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.DataChannelEstablished,
},
}
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this)
// Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
this.onEngineConnectionOpen(this)
})
this.unreliableDataChannel.addEventListener('close', (event) => {
console.log(event)
console.log('unreliable data channel closed')
this.disconnectAll()
this.unreliableDataChannel = undefined
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
})
this.unreliableDataChannel.addEventListener('error', (event) => {
this.disconnectAll()
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 = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer' this.websocket.binaryType = 'arraybuffer'
this.pc = new RTCPeerConnection()
this.pc.createDataChannel('unreliable_modeling_cmds')
this.websocket.addEventListener('open', (event) => { this.websocket.addEventListener('open', (event) => {
console.log('Connected to websocket, waiting for ICE servers') 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) { if (this.token) {
this.send({ headers: { Authorization: `Bearer ${this.token}` } }) this.send({ headers: { Authorization: `Bearer ${this.token}` } })
} }
})
this.pc.addEventListener('icecandidateerror', (_event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
console.error(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
})
this.pc.addEventListener('connectionstatechange', (event) => {
if (this.pc?.iceConnectionState === 'connected') {
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
} else if (this.pc?.iceConnectionState === 'failed') {
// failed is a terminal state; let's explicitly kill the
// connection to the server at this point.
console.log('failed to negotiate ice connection; restarting')
this.close()
}
})
this.websocket.addEventListener('open', (event) => {
if (this.shouldTrace()) { if (this.shouldTrace()) {
websocketSpan.resolve?.() websocketSpan.resolve?.()
handshakeSpan = new SpanPromise( handshakeSpan = spanStart('handshake')
webrtcMediaTransaction.startChild({ op: 'handshake' }) iceSpan = spanStart('ice')
) dataChannelSpan = spanStart('data-channel')
iceSpan = new SpanPromise( mediaTrackSpan = spanStart('media-track')
webrtcMediaTransaction.startChild({ op: 'ice' })
)
dataChannelSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'data-channel',
})
)
mediaTrackSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'media-track',
})
)
} }
if (this.shouldTrace()) { if (this.shouldTrace()) {
Promise.all([ void Promise.all([
handshakeSpan.promise, handshakeSpan.promise,
iceSpan.promise, iceSpan.promise,
dataChannelSpan.promise, dataChannelSpan.promise,
@ -269,18 +554,30 @@ class EngineConnection {
webrtcMediaTransaction?.finish() webrtcMediaTransaction?.finish()
}) })
} }
this.onWebsocketOpen(this)
}) })
this.websocket.addEventListener('close', (event) => { this.websocket.addEventListener('close', (event) => {
console.log('websocket connection closed', event) this.disconnectAll()
this.close() this.websocket = undefined
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
}) })
this.websocket.addEventListener('error', (event) => { this.websocket.addEventListener('error', (event) => {
console.log('websocket connection error', event) this.disconnectAll()
this.close()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: new Error(event.toString()),
},
}
}) })
this.websocket.addEventListener('message', (event) => { this.websocket.addEventListener('message', (event) => {
@ -314,28 +611,137 @@ class EngineConnection {
} }
let resp = message.resp let resp = message.resp
if (!resp) {
// If there's no body to the response, we can bail here. // 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 return
} }
if (resp.type === 'sdp_answer') { console.log('received', resp)
let answer = resp.data?.answer
if (!answer || answer.type === 'unspecified') {
return
}
if (this.pc?.signalingState !== 'stable') { switch (resp.type) {
// If the connection is stable, we shouldn't bother updating the case 'ice_server_info':
// SDP, since we have a stable connection to the backend. If we let ice_servers = resp.data?.ice_servers
// need to renegotiate, the whole PeerConnection needs to get
// tore down. // Now that we have some ICE servers it makes sense
this.pc?.setRemoteDescription( // to start initializing the RTCPeerConnection. RTCPeerConnection
new RTCSessionDescription({ // will begin the ICE process.
type: answer.type, createPeerConnection()
sdp: answer.sdp,
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()) { if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to // When both ends have a local and remote SDP, we've been able to
@ -343,194 +749,46 @@ class EngineConnection {
// servers, but this is hand-shook. // servers, but this is hand-shook.
handshakeSpan.resolve?.() handshakeSpan.resolve?.()
} }
} break
} 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) { case 'trickle_ice':
// When we set the Configuration, we want to always force let candidate = resp.data?.candidate
// iceTransportPolicy to 'relay', since we know the topology console.log('trickle_ice: using this candidate: ', candidate)
// of the ICE/STUN/TUN server and the engine. We don't wish to void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
// talk to the engine in any configuration /other/ than relay break
// 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 case 'metrics_request':
// start adding things we care about to the PeerConnection and let if (this.webrtcStatsCollector === undefined) {
// ICE negotiation happen in the background. Everything from here // TODO: Error message 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) => {
const mediaStream = event.streams[0]
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) {
reject(new Error('too many video tracks to report'))
return return
} }
void this.webrtcStatsCollector().then((client_metrics) => {
let videoTrack = mediaStream.getVideoTracks()[0] this.send({
this.pc?.getStats(videoTrack).then((videoTrackStats) => { type: 'metrics_response',
let client_metrics: ClientMetrics = { metrics: client_metrics,
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
rtc_frames_received: 0,
rtc_frames_per_second: 0,
rtc_freeze_count: 0,
rtc_jitter_sec: 0.0,
rtc_keyframes_decoded: 0,
rtc_total_freezes_duration_sec: 0.0,
}
// TODO(paultag): Since we can technically have multiple WebRTC
// video tracks (even if the Server doesn't at the moment), we
// ought to send stats for every video track(?), and add the stream
// ID into it. This raises the cardinality of collected metrics
// when/if we do, but for now, just report the one stream.
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
client_metrics.rtc_frames_decoded =
videoTrackReport.framesDecoded || 0
client_metrics.rtc_frames_dropped =
videoTrackReport.framesDropped || 0
client_metrics.rtc_frames_received =
videoTrackReport.framesReceived || 0
client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0
client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded || 0
client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent,
}
}) })
resolve(client_metrics)
}) })
}) break
} }
this.onNewTrack({
conn: this,
mediaStream: mediaStream,
})
})
this.pc.addEventListener('datachannel', (event) => {
this.unreliableDataChannel = event.channel
console.log('accepted unreliable data channel', event.channel.label)
this.unreliableDataChannel.addEventListener('open', (event) => {
console.log('unreliable data channel opened', event)
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this)
this.ready = true
this.connecting = false
// 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.unreliableDataChannel.addEventListener('close', (event) => {
console.log('unreliable data channel closed')
this.close()
})
this.unreliableDataChannel.addEventListener('error', (event) => {
console.log('unreliable data channel error')
this.close()
})
}) })
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') areAllConnectionsClosed() {
clearTimeout(this.failedConnTimeout) console.log(this.websocket, this.pc, this.unreliableDataChannel)
this.failedConnTimeout = null return !this.websocket && !this.pc && !this.unreliableDataChannel
}
this.onClose(this)
this.ready = false
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

View File

@ -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 languagecalled <code>kcl</code> difficult route of writing our own languagecalled <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.

View File

@ -1441,9 +1441,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -1451,7 +1451,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http 0.2.9", "http 0.2.9",
"indexmap 1.9.3", "indexmap 2.0.2",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -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",
@ -1943,7 +1944,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan" name = "kittycad-execution-plan"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9200b9540fa5ae99b692db276c625223116f467f" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#935256e4a7080ea130b09b578e16820dc96e78e4"
dependencies = [ dependencies = [
"bytes", "bytes",
"insta", "insta",
@ -1972,7 +1973,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-macros" name = "kittycad-execution-plan-macros"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9200b9540fa5ae99b692db276c625223116f467f" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#935256e4a7080ea130b09b578e16820dc96e78e4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1992,8 +1993,8 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.1.11" version = "0.1.12"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9200b9540fa5ae99b692db276c625223116f467f" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#935256e4a7080ea130b09b578e16820dc96e78e4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2020,7 +2021,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-session" name = "kittycad-modeling-session"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9200b9540fa5ae99b692db276c625223116f467f" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#935256e4a7080ea130b09b578e16820dc96e78e4"
dependencies = [ dependencies = [
"futures", "futures",
"kittycad", "kittycad",

View File

@ -2,11 +2,10 @@ use kcl_lib::ast::types::LiteralIdentifier;
use kcl_lib::ast::types::LiteralValue; use kcl_lib::ast::types::LiteralValue;
use crate::CompileError; use crate::CompileError;
use crate::KclFunction;
use super::native_functions; use super::native_functions;
use super::Address; use super::Address;
use super::KclFunction;
use super::String2;
use std::collections::HashMap; use std::collections::HashMap;
@ -21,6 +20,14 @@ pub enum EpBinding {
Sequence(Vec<EpBinding>), Sequence(Vec<EpBinding>),
/// A sequence of KCL values, indexed by their identifier. /// A sequence of KCL values, indexed by their identifier.
Map(HashMap<String, EpBinding>), Map(HashMap<String, EpBinding>),
/// Not associated with a KCEP address.
Function(KclFunction),
}
impl From<KclFunction> for EpBinding {
fn from(f: KclFunction) -> Self {
Self::Function(f)
}
} }
impl EpBinding { impl EpBinding {
@ -31,16 +38,18 @@ impl EpBinding {
LiteralIdentifier::Literal(litval) => match litval.value { LiteralIdentifier::Literal(litval) => match litval.value {
// Arrays can be indexed by integers. // Arrays can be indexed by integers.
LiteralValue::IInteger(i) => match self { LiteralValue::IInteger(i) => match self {
EpBinding::Single(_) => Err(CompileError::CannotIndex),
EpBinding::Sequence(seq) => { EpBinding::Sequence(seq) => {
let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?; let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?;
seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() }) seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() })
} }
EpBinding::Map(_) => Err(CompileError::CannotIndex), EpBinding::Map(_) => Err(CompileError::CannotIndex),
EpBinding::Single(_) => Err(CompileError::CannotIndex),
EpBinding::Function(_) => Err(CompileError::CannotIndex),
}, },
// Objects can be indexed by string properties. // Objects can be indexed by string properties.
LiteralValue::String(property) => match self { LiteralValue::String(property) => match self {
EpBinding::Single(_) => Err(CompileError::NoProperties), EpBinding::Single(_) => Err(CompileError::NoProperties),
EpBinding::Function(_) => Err(CompileError::NoProperties),
EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties), EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties),
EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }), EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }),
}, },
@ -67,7 +76,6 @@ pub struct BindingScope {
// KCL value which are stored in EP memory. // KCL value which are stored in EP memory.
ep_bindings: HashMap<String, EpBinding>, ep_bindings: HashMap<String, EpBinding>,
/// KCL functions. They do NOT get stored in EP memory. /// KCL functions. They do NOT get stored in EP memory.
function_bindings: HashMap<String2, Box<dyn KclFunction>>,
parent: Option<Box<BindingScope>>, parent: Option<Box<BindingScope>>,
} }
@ -80,29 +88,39 @@ impl BindingScope {
Self { Self {
// TODO: Actually put the stdlib prelude in here, // TODO: Actually put the stdlib prelude in here,
// things like `startSketchAt` and `line`. // things like `startSketchAt` and `line`.
function_bindings: HashMap::from([ ep_bindings: HashMap::from([
("id".into(), Box::new(native_functions::Id) as _), ("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))),
("add".into(), Box::new(native_functions::Add) as _), ("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
(
"startSketchAt".into(),
EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)),
),
]), ]),
ep_bindings: Default::default(),
parent: None, parent: None,
} }
} }
/// Add a new scope, e.g. for new function calls. /// Add a new scope, e.g. for new function calls.
#[allow(dead_code)] // TODO: when we implement function expressions. pub fn add_scope(&mut self) {
pub fn add_scope(self) -> Self { // Move all data from `self` into `this`.
Self { let this_parent = self.parent.take();
function_bindings: Default::default(), let this_ep_bindings = self.ep_bindings.drain().collect();
ep_bindings: Default::default(), let this = Self {
parent: Some(Box::new(self)), ep_bindings: this_ep_bindings,
} parent: this_parent,
};
// Turn `self` into a new scope, with the old `self` as its parent.
self.parent = Some(Box::new(this));
} }
//// Remove a scope, e.g. when exiting a function call. //// Remove a scope, e.g. when exiting a function call.
#[allow(dead_code)] // TODO: when we implement function expressions. pub fn remove_scope(&mut self) {
pub fn remove_scope(self) -> Self { // The scope is finished, so erase all its local variables.
*self.parent.unwrap() self.ep_bindings.clear();
// Pop the stack -- the parent scope is now the current scope.
let p = self.parent.take().expect("cannot remove the root scope");
self.parent = p.parent;
self.ep_bindings = p.ep_bindings;
} }
/// Add a binding (e.g. defining a new variable) /// Add a binding (e.g. defining a new variable)
@ -126,10 +144,11 @@ impl BindingScope {
/// Look up a function bound to the given identifier. /// Look up a function bound to the given identifier.
pub fn get_fn(&self, identifier: &str) -> GetFnResult { pub fn get_fn(&self, identifier: &str) -> GetFnResult {
if let Some(f) = self.function_bindings.get(identifier) { if let Some(x) = self.get(identifier) {
GetFnResult::Found(f.as_ref()) match x {
} else if self.get(identifier).is_some() { EpBinding::Function(f) => GetFnResult::Found(f),
GetFnResult::NonCallable _ => GetFnResult::NonCallable,
}
} else if let Some(ref parent) = self.parent { } else if let Some(ref parent) = self.parent {
parent.get_fn(identifier) parent.get_fn(identifier)
} else { } else {
@ -139,7 +158,7 @@ impl BindingScope {
} }
pub enum GetFnResult<'a> { pub enum GetFnResult<'a> {
Found(&'a dyn KclFunction), Found(&'a KclFunction),
NonCallable, NonCallable,
NotFound, NotFound,
} }

View 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),
}

View File

@ -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>),
@ -18,6 +19,8 @@ pub enum SingleValue {
UnaryExpression(Box<ast::types::UnaryExpression>), UnaryExpression(Box<ast::types::UnaryExpression>),
KclNoneExpression(ast::types::KclNone), KclNoneExpression(ast::types::KclNone),
MemberExpression(Box<ast::types::MemberExpression>), MemberExpression(Box<ast::types::MemberExpression>),
FunctionExpression(Box<ast::types::FunctionExpression>),
PipeSubstitution(Box<ast::types::PipeSubstitution>),
} }
impl From<ast::types::BinaryPart> for KclValueGroup { impl From<ast::types::BinaryPart> for KclValueGroup {
@ -59,7 +62,8 @@ impl From<ast::types::Value> for KclValueGroup {
ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e), ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e),
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::PipeSubstitution(_) | ast::types::Value::FunctionExpression(_) => todo!(), ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
ast::types::Value::PipeSubstitution(e) => Self::Single(SingleValue::PipeSubstitution(e)),
} }
} }
} }
@ -76,6 +80,8 @@ impl From<KclValueGroup> for ast::types::Value {
SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e), SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e),
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::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),

View File

@ -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,21 +9,22 @@ use std::collections::HashMap;
use kcl_lib::{ use kcl_lib::{
ast, ast,
ast::types::{BodyItem, KclNone, LiteralValue, Program}, 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.
pub async fn execute(ast: Program, session: Session) -> Result<(), Error> { pub async fn execute(ast: Program, session: Session) -> Result<(), Error> {
let mut planner = Planner::new(); let mut planner = Planner::new();
let plan = planner.build_plan(ast)?; let (plan, _retval) = planner.build_plan(ast)?;
let mut mem = kittycad_execution_plan::Memory::default(); let mut mem = kittycad_execution_plan::Memory::default();
kittycad_execution_plan::execute(&mut mem, plan, session).await?; kittycad_execution_plan::execute(&mut mem, plan, session).await?;
Ok(()) Ok(())
@ -44,25 +46,42 @@ impl Planner {
} }
} }
fn build_plan(&mut self, program: Program) -> PlanRes { /// If successful, return the KCEP instructions for executing the given program.
program.body.into_iter().try_fold(Vec::new(), |mut instructions, item| { /// If the program is a function with a return, then it also returns the KCL function's return value.
let instructions_for_this_node = match item { fn build_plan(&mut self, program: Program) -> Result<(Vec<Instruction>, Option<EpBinding>), CompileError> {
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) { program
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?.instructions, .body
KclValueGroup::ArrayExpression(_) => todo!(), .into_iter()
KclValueGroup::ObjectExpression(_) => todo!(), .try_fold((Vec::new(), None), |(mut instructions, mut retval), item| {
}, if retval.is_some() {
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?, return Err(CompileError::MultipleReturns);
BodyItem::ReturnStatement(_) => todo!(), }
}; let mut ctx = Context::default();
instructions.extend(instructions_for_this_node); let instructions_for_this_node = match item {
Ok(instructions) BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
}) KclValueGroup::Single(value) => self.plan_to_compute_single(&mut ctx, value)?.instructions,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
},
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) {
KclValueGroup::Single(value) => {
let EvalPlan { instructions, binding } = self.plan_to_compute_single(&mut ctx, value)?;
retval = Some(binding);
instructions
}
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
},
};
instructions.extend(instructions_for_this_node);
Ok((instructions, retval))
})
} }
/// 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);
@ -74,6 +93,23 @@ impl Planner {
binding: EpBinding::Single(address), binding: EpBinding::Single(address),
}) })
} }
SingleValue::FunctionExpression(expr) => {
let FunctionExpressionParts {
start: _,
end: _,
params_required,
params_optional,
body,
} = expr.into_parts().map_err(CompileError::BadParamOrder)?;
Ok(EvalPlan {
instructions: Vec::new(),
binding: EpBinding::from(KclFunction::UserDefined(UserDefinedFunction {
params_optional,
params_required,
body,
})),
})
}
SingleValue::Literal(expr) => { SingleValue::Literal(expr) => {
let kcep_val = kcl_literal_to_kcep_literal(expr.value); let kcep_val = kcl_literal_to_kcep_literal(expr.value);
// KCEP primitives always have size of 1, because each address holds 1 primitive. // KCEP primitives always have size of 1, because each address holds 1 primitive.
@ -88,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
@ -100,9 +156,33 @@ impl Planner {
binding: previously_bound_to.clone(), binding: previously_bound_to.clone(),
}) })
} }
SingleValue::UnaryExpression(expr) => {
let operand = self.plan_to_compute_single(ctx, SingleValue::from(expr.argument))?;
let EpBinding::Single(binding) = operand.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let destination = self.next_addr.offset_by(1);
let mut plan = operand.instructions;
plan.push(Instruction::UnaryArithmetic {
arithmetic: ep::UnaryArithmetic {
operation: match expr.operator {
ast::types::UnaryOperator::Neg => ep::UnaryOperation::Neg,
ast::types::UnaryOperator::Not => ep::UnaryOperation::Not,
},
operand: ep::Operand::Reference(binding),
},
destination,
});
Ok(EvalPlan {
instructions: plan,
binding: EpBinding::Single(destination),
})
}
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",
@ -117,13 +197,13 @@ impl Planner {
let mut plan = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1); let mut plan = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1);
plan.extend(l.instructions); plan.extend(l.instructions);
plan.extend(r.instructions); plan.extend(r.instructions);
plan.push(Instruction::Arithmetic { plan.push(Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: match expr.operator { operation: match expr.operator {
ast::types::BinaryOperator::Add => ep::Operation::Add, ast::types::BinaryOperator::Add => ep::BinaryOperation::Add,
ast::types::BinaryOperator::Sub => ep::Operation::Sub, ast::types::BinaryOperator::Sub => ep::BinaryOperation::Sub,
ast::types::BinaryOperator::Mul => ep::Operation::Mul, ast::types::BinaryOperator::Mul => ep::BinaryOperation::Mul,
ast::types::BinaryOperator::Div => ep::Operation::Div, ast::types::BinaryOperator::Div => ep::BinaryOperation::Div,
ast::types::BinaryOperator::Mod => { ast::types::BinaryOperator::Mod => {
todo!("execution plan instruction set doesn't support Mod yet") todo!("execution plan instruction set doesn't support Mod yet")
} }
@ -142,6 +222,7 @@ impl Planner {
}) })
} }
SingleValue::CallExpression(expr) => { SingleValue::CallExpression(expr) => {
// Make a plan to compute all the arguments to this call.
let (mut instructions, args) = expr.arguments.into_iter().try_fold( let (mut instructions, args) = expr.arguments.into_iter().try_fold(
(Vec::new(), Vec::new()), (Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_args), argument| { |(mut acc_instrs, mut acc_args), argument| {
@ -149,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!(),
}; };
@ -158,6 +239,7 @@ impl Planner {
Ok((acc_instrs, acc_args)) Ok((acc_instrs, acc_args))
}, },
)?; )?;
// Look up the function being called.
let callee = match self.binding_scope.get_fn(&expr.callee.name) { let callee = match self.binding_scope.get_fn(&expr.callee.name) {
GetFnResult::Found(f) => f, GetFnResult::Found(f) => f,
GetFnResult::NonCallable => { GetFnResult::NonCallable => {
@ -172,10 +254,67 @@ impl Planner {
} }
}; };
// Emit instructions to call that function with the given arguments.
use native_functions::Callable;
let EvalPlan { let EvalPlan {
instructions: eval_instrs, instructions: eval_instrs,
binding, binding,
} = callee.call(&mut self.next_addr, args)?; } = match callee {
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
KclFunction::UserDefined(f) => {
let UserDefinedFunction {
params_optional,
params_required,
body: function_body,
} = f.clone();
let num_required_params = params_required.len();
self.binding_scope.add_scope();
// Bind the call's arguments to the names of the function's parameters.
let num_actual_params = args.len();
let mut arg_iter = args.into_iter();
let max_params = params_required.len() + params_optional.len();
if num_actual_params > max_params {
return Err(CompileError::TooManyArgs {
fn_name: "".into(),
maximum: max_params,
actual: num_actual_params,
});
}
// Bind required parameters
for param in params_required {
let arg = arg_iter.next().ok_or(CompileError::NotEnoughArgs {
fn_name: "".into(),
required: num_required_params,
actual: num_actual_params,
})?;
self.binding_scope.bind(param.identifier.name, arg);
}
// Bind optional parameters
for param in params_optional {
let Some(arg) = arg_iter.next() else {
break;
};
self.binding_scope.bind(param.identifier.name, arg);
}
let (instructions, retval) = self.build_plan(function_body)?;
let Some(retval) = retval else {
return Err(CompileError::NoReturnStmt);
};
self.binding_scope.remove_scope();
EvalPlan {
instructions,
binding: retval,
}
}
};
// Combine the "evaluate arguments" plan with the "call function" plan.
instructions.extend(eval_instrs); instructions.extend(eval_instrs);
Ok(EvalPlan { instructions, binding }) Ok(EvalPlan { instructions, binding })
} }
@ -207,8 +346,56 @@ impl Planner {
binding: binding.clone(), binding: binding.clone(),
}) })
} }
SingleValue::PipeExpression(_) => todo!(), SingleValue::PipeSubstitution(_expr) => {
SingleValue::UnaryExpression(_) => todo!(), 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,
})
}
} }
} }
@ -219,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)
@ -232,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) => {
@ -251,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);
} }
@ -264,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)
@ -282,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)
@ -304,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);
} }
@ -317,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)
@ -335,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)
@ -353,52 +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 },
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Compile(#[from] CompileError),
#[error("{0}")]
Execution(#[from] ExecutionError),
}
type PlanRes = Result<Vec<Instruction>, CompileError>;
/// 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 {
@ -417,9 +560,35 @@ struct EvalPlan {
binding: EpBinding, binding: EpBinding,
} }
trait KclFunction: std::fmt::Debug {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
}
/// Either an owned string, or a static string. Either way it can be read and moved around. /// Either an owned string, or a static string. Either way it can be read and moved around.
pub type String2 = std::borrow::Cow<'static, str>; pub type String2 = std::borrow::Cow<'static, str>;
#[derive(Debug, Clone)]
struct UserDefinedFunction {
params_optional: Vec<ast::types::Parameter>,
params_required: Vec<ast::types::Parameter>,
body: ast::types::Program,
}
impl PartialEq for UserDefinedFunction {
fn eq(&self, other: &Self) -> bool {
self.params_optional == other.params_optional && self.params_required == other.params_required
}
}
impl Eq for UserDefinedFunction {}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
enum KclFunction {
Id(native_functions::Id),
StartSketchAt(native_functions::StartSketchAt),
Add(native_functions::Add),
UserDefined(UserDefinedFunction),
}
/// Context used when compiling KCL.
#[derive(Default, Debug)]
struct Context {
pipe_substitution: Option<EpBinding>,
}

View File

@ -3,16 +3,21 @@
//! But some other stdlib functions will be written in KCL. //! But some other stdlib functions will be written in KCL.
use kcl_lib::std::sketch::PlaneData; use kcl_lib::std::sketch::PlaneData;
use kittycad_execution_plan::{Address, Arithmetic, Instruction}; use kittycad_execution_plan::{Address, BinaryArithmetic, Instruction};
use kittycad_execution_plan_traits::Value; use kittycad_execution_plan_traits::Value;
use crate::{CompileError, EpBinding, EvalPlan, KclFunction}; use crate::{CompileError, EpBinding, EvalPlan};
/// The identity function. Always returns its first input. /// The identity function. Always returns its first input.
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Id; pub struct Id;
impl KclFunction for Id { pub trait Callable {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
}
impl Callable for Id {
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> { fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 { if args.len() > 1 {
return Err(CompileError::TooManyArgs { return Err(CompileError::TooManyArgs {
@ -36,10 +41,11 @@ impl KclFunction for Id {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct StartSketchAt; pub struct StartSketchAt;
impl KclFunction for StartSketchAt { impl Callable for StartSketchAt {
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> { fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new(); let mut instructions = Vec::new();
// Store the plane. // Store the plane.
@ -64,10 +70,11 @@ impl KclFunction for StartSketchAt {
} }
/// A test function that adds two numbers. /// A test function that adds two numbers.
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Add; pub struct Add;
impl KclFunction for Add { impl Callable for Add {
fn call(&self, next_address: &mut Address, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> { fn call(&self, next_address: &mut Address, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let len = args.len(); let len = args.len();
if len > 2 { if len > 2 {
@ -91,9 +98,9 @@ impl KclFunction for Add {
}; };
let destination = next_address.offset_by(1); let destination = next_address.offset_by(1);
Ok(EvalPlan { Ok(EvalPlan {
instructions: vec![Instruction::Arithmetic { instructions: vec![Instruction::BinaryArithmetic {
arithmetic: Arithmetic { arithmetic: BinaryArithmetic {
operation: kittycad_execution_plan::Operation::Add, operation: kittycad_execution_plan::BinaryOperation::Add,
operand0: kittycad_execution_plan::Operand::Reference(arg0), operand0: kittycad_execution_plan::Operand::Reference(arg0),
operand1: kittycad_execution_plan::Operand::Reference(arg1), operand1: kittycad_execution_plan::Operand::Reference(arg1),
}, },

View File

@ -1,3 +1,4 @@
use ep::UnaryArithmetic;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
@ -7,7 +8,7 @@ fn must_plan(program: &str) -> (Vec<Instruction>, BindingScope) {
let parser = kcl_lib::parser::Parser::new(tokens); let parser = kcl_lib::parser::Parser::new(tokens);
let ast = parser.ast().unwrap(); let ast = parser.ast().unwrap();
let mut p = Planner::new(); let mut p = Planner::new();
let instrs = p.build_plan(ast).unwrap(); let (instrs, _) = p.build_plan(ast).unwrap();
(instrs, p.binding_scope) (instrs, p.binding_scope)
} }
@ -109,6 +110,17 @@ fn bind_arrays_with_objects_elements() {
); );
} }
#[test]
fn statement_after_return() {
let program = "fn f = () => {
return 1
let x = 2
}
f()";
let err = should_not_compile(program);
assert_eq!(err, CompileError::MultipleReturns);
}
#[test] #[test]
fn name_not_found() { fn name_not_found() {
// Users can't assign `y` to anything because `y` is undefined. // Users can't assign `y` to anything because `y` is undefined.
@ -116,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 = "
@ -146,9 +175,9 @@ fn use_native_function_add() {
address: Address::ZERO.offset(1), address: Address::ZERO.offset(1),
value: 2i64.into() value: 2i64.into()
}, },
Instruction::Arithmetic { Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: ep::Operation::Add, operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(Address::ZERO), operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1)) operand1: ep::Operand::Reference(Address::ZERO.offset(1))
}, },
@ -252,6 +281,27 @@ fn member_expressions_array() {
} }
} }
#[test]
fn compile_flipped_sign() {
let program = "let x = 3
let y = -x";
let (plan, _scope) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 3i64.into(),
},
Instruction::UnaryArithmetic {
arithmetic: UnaryArithmetic {
operation: ep::UnaryOperation::Neg,
operand: ep::Operand::Reference(Address::ZERO),
},
destination: Address::ZERO + 1,
},
];
assert_eq!(plan, expected);
}
#[test] #[test]
fn add_literals() { fn add_literals() {
let program = "let x = 1 + 2"; let program = "let x = 1 + 2";
@ -267,9 +317,9 @@ fn add_literals() {
address: Address::ZERO.offset(1), address: Address::ZERO.offset(1),
value: 2i64.into() value: 2i64.into()
}, },
Instruction::Arithmetic { Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: ep::Operation::Add, operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(Address::ZERO), operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1)), operand1: ep::Operand::Reference(Address::ZERO.offset(1)),
}, },
@ -299,9 +349,9 @@ fn add_vars() {
address: addr1, address: addr1,
value: 2i64.into(), value: 2i64.into(),
}, },
Instruction::Arithmetic { Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: ep::Operation::Add, operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr0), operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1), operand1: ep::Operand::Reference(addr1),
}, },
@ -340,18 +390,18 @@ fn composite_binary_exprs() {
value: 3i64.into(), value: 3i64.into(),
}, },
// Adds 1 + 2 // Adds 1 + 2
Instruction::Arithmetic { Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: ep::Operation::Add, operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr0), operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1), operand1: ep::Operand::Reference(addr1),
}, },
destination: addr3, destination: addr3,
}, },
// Adds `x` + 3, where `x` is (1 + 2) // Adds `x` + 3, where `x` is (1 + 2)
Instruction::Arithmetic { Instruction::BinaryArithmetic {
arithmetic: ep::Arithmetic { arithmetic: ep::BinaryArithmetic {
operation: ep::Operation::Add, operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr3), operand0: ep::Operand::Reference(addr3),
operand1: ep::Operand::Reference(addr2), operand1: ep::Operand::Reference(addr2),
}, },
@ -361,6 +411,238 @@ fn composite_binary_exprs() {
); );
} }
#[test]
fn use_kcl_functions_zero_params() {
let (plan, scope) = must_plan(
"fn triple = () => { return 123 }
let x = triple()",
);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 123i64.into()
}]
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}");
}
}
}
#[test]
fn use_kcl_functions_with_optional_params() {
for (i, program) in ["fn triple = (x, y?) => { return x*3 }
let x = triple(1, 888)"]
.into_iter()
.enumerate()
{
let (plan, scope) = must_plan(program);
let destination = Address::ZERO + 3;
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 888i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 3i64.into(),
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Mul,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO + 2)
},
destination,
}
],
"failed test {i}"
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &destination, "failed test {i}");
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}");
}
}
}
}
#[test]
fn use_kcl_functions_with_too_many_params() {
let program = "fn triple = (x, y?) => { return x*3 }
let x = triple(1, 2, 3)";
let err = should_not_compile(program);
assert!(matches!(
err,
CompileError::TooManyArgs {
maximum: 2,
actual: 3,
..
}
))
}
#[test]
fn use_kcl_function_as_return_value() {
let program = "fn twotwotwo = () => {
return () => { return 222 }
}
let f = twotwotwo()
let x = f()";
let (plan, scope) = must_plan(program);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test");
}
}
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 222i64.into()
}]
)
}
#[test]
fn define_recursive_function() {
let program = "fn add_infinitely = (i) => {
return add_infinitely(i+1)
}";
let (plan, _scope) = must_plan(program);
assert_eq!(plan, Vec::new())
}
#[test]
fn use_kcl_function_as_param() {
let program = "fn wrapper = (f) => {
return f()
}
fn twotwotwo = () => {
return 222
}
let x = wrapper(twotwotwo)";
let (plan, scope) = must_plan(program);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test");
}
}
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 222i64.into()
}]
)
}
#[test]
fn use_kcl_functions_with_params() {
for (i, program) in [
"fn triple = (x) => { return x*3 }
let x = triple(1)",
"fn triple = (x,y?) => { return x*3 }
let x = triple(1)",
]
.into_iter()
.enumerate()
{
let (plan, scope) = must_plan(program);
let destination = Address::ZERO + 2;
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 3i64.into(),
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Mul,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
},
destination,
}
],
"failed test {i}"
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &destination, "failed test {i}");
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}");
}
}
}
}
#[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]
fn define_kcl_functions() {
let (plan, scope) = must_plan("fn triple = (x) => { return x * 3 }");
assert!(plan.is_empty());
match scope.get("triple").unwrap() {
EpBinding::Function(KclFunction::UserDefined(expr)) => {
assert!(expr.params_optional.is_empty());
assert_eq!(expr.params_required.len(), 1);
}
other => {
panic!("expected 'triple' bound to a user-defined KCL function but it was bound to {other:?}");
}
}
}
#[test] #[test]
fn aliases_dont_affect_plans() { fn aliases_dont_affect_plans() {
let (plan1, _) = must_plan( let (plan1, _) = must_plan(

View File

@ -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" }

View File

@ -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;
@ -2631,7 +2644,7 @@ async fn execute_pipe_body(
} }
/// Parameter of a KCL function. /// Parameter of a KCL function.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -2655,6 +2668,23 @@ pub struct FunctionExpression {
impl_value_meta!(FunctionExpression); impl_value_meta!(FunctionExpression);
pub struct FunctionExpressionParts {
pub start: usize,
pub end: usize,
pub params_required: Vec<Parameter>,
pub params_optional: Vec<Parameter>,
pub body: Program,
}
#[derive(Debug, PartialEq, Clone)]
pub struct RequiredParamAfterOptionalParam(pub Parameter);
impl std::fmt::Display for RequiredParamAfterOptionalParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "KCL functions must declare any optional parameters after all the required parameters. But your required parameter {} is _after_ an optional parameter. You must move it to before the optional parameters instead.", self.0.identifier.name)
}
}
impl FunctionExpression { impl FunctionExpression {
/// Function expressions don't really apply. /// Function expressions don't really apply.
pub fn get_constraint_level(&self) -> ConstraintLevel { pub fn get_constraint_level(&self) -> ConstraintLevel {
@ -2663,6 +2693,36 @@ impl FunctionExpression {
} }
} }
pub fn into_parts(self) -> Result<FunctionExpressionParts, RequiredParamAfterOptionalParam> {
let Self {
start,
end,
params,
body,
} = self;
let mut params_required = Vec::with_capacity(params.len());
let mut params_optional = Vec::with_capacity(params.len());
for param in params {
if param.optional {
params_optional.push(param);
} else {
if !params_optional.is_empty() {
return Err(RequiredParamAfterOptionalParam(param));
}
params_required.push(param);
}
}
params_required.shrink_to_fit();
params_optional.shrink_to_fit();
Ok(FunctionExpressionParts {
start,
end,
params_required,
params_optional,
body,
})
}
/// Required parameters must be declared before optional parameters. /// Required parameters must be declared before optional parameters.
/// This gets all the required parameters. /// This gets all the required parameters.
pub fn required_params(&self) -> &[Parameter] { pub fn required_params(&self) -> &[Parameter] {

View File

@ -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(_) => {}

View File

@ -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()
}], }],
})) }))
} }

View File

@ -8188,10 +8188,10 @@ vite-tsconfig-paths@^4.2.1:
globrex "^0.1.2" globrex "^0.1.2"
tsconfck "^2.1.0" tsconfck "^2.1.0"
"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.1: "vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.2:
version "4.5.1" version "4.5.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
dependencies: dependencies:
esbuild "^0.18.10" esbuild "^0.18.10"
postcss "^8.4.27" postcss "^8.4.27"