Compare commits
1 Commits
path-to-no
...
achalmers/
| Author | SHA1 | Date | |
|---|---|---|---|
| 153d54c88f |
@ -1,3 +1,3 @@
|
|||||||
[codespell]
|
[codespell]
|
||||||
ignore-words-list: crate,everytime
|
ignore-words-list: crate,everytime
|
||||||
skip: **/target,node_modules,build,**/Cargo.lock
|
skip: **/target,node_modules,build
|
||||||
|
|||||||
@ -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.2",
|
"vite": "^4.5.1",
|
||||||
"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
6
src-tauri/Cargo.lock
generated
@ -1242,9 +1242,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.24"
|
version = "0.3.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -1252,7 +1252,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 2.0.0",
|
"indexmap 1.9.3",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { getNormalisedCoordinates, throttle } from '../lib/utils'
|
import { getNormalisedCoordinates } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
@ -115,9 +115,9 @@ export const Stream = ({ className = '' }) => {
|
|||||||
setClickCoords({ x, y })
|
setClickCoords({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
const fps = 60
|
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
|
|
||||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||||
|
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -126,7 +126,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
})
|
})
|
||||||
}, Math.round(1000 / fps))
|
}
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
||||||
clientX,
|
clientX,
|
||||||
|
|||||||
@ -48,72 +48,6 @@ 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.
|
||||||
@ -121,28 +55,10 @@ 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
|
||||||
@ -178,77 +94,74 @@ 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(() => {
|
||||||
switch (this.state.type as EngineConnectionStateType) {
|
if (this.dead) {
|
||||||
case EngineConnectionStateType.ConnectionEstablished:
|
clearInterval(pingInterval)
|
||||||
this.send({ type: 'ping' })
|
}
|
||||||
break
|
if (this.isReady()) {
|
||||||
case EngineConnectionStateType.Disconnected:
|
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
||||||
clearInterval(pingInterval)
|
// command through the WebSocket connection. This will help both ends
|
||||||
break
|
// of the connection maintain the TCP connection without hitting a
|
||||||
default:
|
// timeout condition.
|
||||||
break
|
this.send({ type: 'ping' })
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
let connectRetryInterval = setInterval(() => {
|
let connectInterval = setInterval(() => {
|
||||||
if (this.state.type !== EngineConnectionStateType.Disconnected) return
|
if (this.dead) {
|
||||||
switch (this.state.value.type) {
|
clearInterval(connectInterval)
|
||||||
case DisconnectedType.Error:
|
return
|
||||||
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.state.type === EngineConnectionStateType.Connecting
|
return this.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.state.type === EngineConnectionStateType.ConnectionEstablished
|
return this.ready
|
||||||
}
|
}
|
||||||
|
|
||||||
tearDown() {
|
tearDown() {
|
||||||
this.disconnectAll()
|
this.dead = true
|
||||||
this.state = {
|
this.close()
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -282,269 +195,71 @@ 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({ name: 'webrtc-media' })
|
webrtcMediaTransaction = Sentry.startTransaction({
|
||||||
websocketSpan = spanStart('websocket')
|
name: 'webrtc-media',
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
||||||
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
|
webrtcMediaTransaction.startChild({ op: 'websocket' })
|
||||||
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) => {
|
||||||
this.state = {
|
console.log('Connected to websocket, waiting for ICE servers')
|
||||||
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 = spanStart('handshake')
|
handshakeSpan = new SpanPromise(
|
||||||
iceSpan = spanStart('ice')
|
webrtcMediaTransaction.startChild({ op: 'handshake' })
|
||||||
dataChannelSpan = spanStart('data-channel')
|
)
|
||||||
mediaTrackSpan = spanStart('media-track')
|
iceSpan = new SpanPromise(
|
||||||
|
webrtcMediaTransaction.startChild({ op: 'ice' })
|
||||||
|
)
|
||||||
|
dataChannelSpan = new SpanPromise(
|
||||||
|
webrtcMediaTransaction.startChild({
|
||||||
|
op: 'data-channel',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
mediaTrackSpan = new SpanPromise(
|
||||||
|
webrtcMediaTransaction.startChild({
|
||||||
|
op: 'media-track',
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
void Promise.all([
|
Promise.all([
|
||||||
handshakeSpan.promise,
|
handshakeSpan.promise,
|
||||||
iceSpan.promise,
|
iceSpan.promise,
|
||||||
dataChannelSpan.promise,
|
dataChannelSpan.promise,
|
||||||
@ -554,30 +269,18 @@ class EngineConnection {
|
|||||||
webrtcMediaTransaction?.finish()
|
webrtcMediaTransaction?.finish()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onWebsocketOpen(this)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.websocket.addEventListener('close', (event) => {
|
this.websocket.addEventListener('close', (event) => {
|
||||||
this.disconnectAll()
|
console.log('websocket connection closed', event)
|
||||||
this.websocket = undefined
|
this.close()
|
||||||
|
|
||||||
if (this.areAllConnectionsClosed()) {
|
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Disconnected,
|
|
||||||
value: { type: DisconnectedType.Quit },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.websocket.addEventListener('error', (event) => {
|
this.websocket.addEventListener('error', (event) => {
|
||||||
this.disconnectAll()
|
console.log('websocket connection error', event)
|
||||||
|
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) => {
|
||||||
@ -611,137 +314,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('received', resp)
|
if (resp.type === 'sdp_answer') {
|
||||||
|
let answer = resp.data?.answer
|
||||||
|
if (!answer || answer.type === 'unspecified') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch (resp.type) {
|
if (this.pc?.signalingState !== 'stable') {
|
||||||
case 'ice_server_info':
|
// If the connection is stable, we shouldn't bother updating the
|
||||||
let ice_servers = resp.data?.ice_servers
|
// SDP, since we have a stable connection to the backend. If we
|
||||||
|
// need to renegotiate, the whole PeerConnection needs to get
|
||||||
// Now that we have some ICE servers it makes sense
|
// tore down.
|
||||||
// to start initializing the RTCPeerConnection. RTCPeerConnection
|
this.pc?.setRemoteDescription(
|
||||||
// will begin the ICE process.
|
new RTCSessionDescription({
|
||||||
createPeerConnection()
|
type: answer.type,
|
||||||
|
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
|
||||||
@ -749,46 +343,194 @@ 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
|
||||||
|
|
||||||
case 'trickle_ice':
|
if (ice_servers?.length > 0) {
|
||||||
let candidate = resp.data?.candidate
|
// When we set the Configuration, we want to always force
|
||||||
console.log('trickle_ice: using this candidate: ', candidate)
|
// iceTransportPolicy to 'relay', since we know the topology
|
||||||
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
||||||
break
|
// talk to the engine in any configuration /other/ than relay
|
||||||
|
// from a infra POV.
|
||||||
|
this.pc.setConfiguration({
|
||||||
|
iceServers: ice_servers,
|
||||||
|
iceTransportPolicy: 'relay',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.pc?.setConfiguration({})
|
||||||
|
}
|
||||||
|
|
||||||
case 'metrics_request':
|
// We have an ICE Servers set now. We just setConfiguration, so let's
|
||||||
if (this.webrtcStatsCollector === undefined) {
|
// start adding things we care about to the PeerConnection and let
|
||||||
// TODO: Error message here?
|
// ICE negotiation happen in the background. Everything from here
|
||||||
return
|
// until the end of this function is setup of our end of the
|
||||||
}
|
// PeerConnection and waiting for events to fire our callbacks.
|
||||||
void this.webrtcStatsCollector().then((client_metrics) => {
|
|
||||||
|
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({
|
this.send({
|
||||||
type: 'metrics_response',
|
type: 'trickle_ice',
|
||||||
metrics: client_metrics,
|
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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
break
|
.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
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
this.failedConnTimeout = null
|
console.log('engine connection timeout on connection, closing')
|
||||||
this.disconnectAll()
|
this.close()
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Disconnected,
|
|
||||||
value: {
|
|
||||||
type: DisconnectedType.Timeout,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}, connectionTimeoutMs)
|
}, connectionTimeoutMs)
|
||||||
|
|
||||||
this.onConnectionStarted(this)
|
this.onConnectionStarted(this)
|
||||||
@ -807,15 +549,23 @@ class EngineConnection {
|
|||||||
typeof message === 'string' ? message : JSON.stringify(message)
|
typeof message === 'string' ? message : JSON.stringify(message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disconnectAll() {
|
close() {
|
||||||
this.websocket?.close()
|
this.websocket?.close()
|
||||||
this.unreliableDataChannel?.close()
|
|
||||||
this.pc?.close()
|
this.pc?.close()
|
||||||
|
this.unreliableDataChannel?.close()
|
||||||
|
this.websocket = undefined
|
||||||
|
this.pc = undefined
|
||||||
|
this.unreliableDataChannel = undefined
|
||||||
this.webrtcStatsCollector = undefined
|
this.webrtcStatsCollector = undefined
|
||||||
}
|
if (this.failedConnTimeout) {
|
||||||
areAllConnectionsClosed() {
|
console.log('closed timeout in close')
|
||||||
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
clearTimeout(this.failedConnTimeout)
|
||||||
return !this.websocket && !this.pc && !this.unreliableDataChannel
|
this.failedConnTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onClose(this)
|
||||||
|
this.ready = false
|
||||||
|
this.connecting = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -935,7 +685,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()
|
||||||
void this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: gizmoId,
|
cmd_id: gizmoId,
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -948,7 +698,7 @@ export class EngineCommandManager {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Initialize the planes.
|
// Initialize the planes.
|
||||||
void this.initPlanes().then(() => {
|
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
|
||||||
@ -995,7 +745,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.
|
||||||
void exportSave(event.data)
|
exportSave(event.data)
|
||||||
} else {
|
} else {
|
||||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||||
event.data
|
event.data
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
|
|||||||
} else if (!currentPath) {
|
} else if (!currentPath) {
|
||||||
return [0, 0]
|
return [0, 0]
|
||||||
}
|
}
|
||||||
if (currentPath.type === 'topoint') {
|
if (currentPath.type === 'toPoint') {
|
||||||
return [currentPath.to[0], currentPath.to[1]]
|
return [currentPath.to[0], currentPath.to[1]]
|
||||||
}
|
}
|
||||||
return [0, 0]
|
return [0, 0]
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function CodeEditor() {
|
|||||||
The left pane is where you write your code. It's a code editor with
|
The left pane is where you write your code. It's a code editor with
|
||||||
syntax highlighting and autocompletion. We've decided to take the
|
syntax highlighting and autocompletion. We've decided to take the
|
||||||
difficult route of writing our own language—called <code>kcl</code>
|
difficult route of writing our own language—called <code>kcl</code>
|
||||||
—for describing geometry, because we don't want to inherit all the
|
—for describing geometry, because 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.
|
||||||
|
|||||||
4
src/wasm-lib/.config/cargo.toml
Normal file
4
src/wasm-lib/.config/cargo.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-args=-z stack-size=1500000",
|
||||||
|
]
|
||||||
1575
src/wasm-lib/Cargo.lock
generated
1575
src/wasm-lib/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ image = "0.24.7"
|
|||||||
kittycad = { workspace = true, default-features = true }
|
kittycad = { workspace = true, default-features = true }
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
reqwest = { version = "0.11.22", default-features = false }
|
reqwest = { version = "0.11.22", default-features = false }
|
||||||
tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
twenty-twenty = "0.7"
|
twenty-twenty = "0.7"
|
||||||
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
||||||
|
|
||||||
@ -52,17 +52,12 @@ debug = true
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"derive-docs",
|
"derive-docs",
|
||||||
"grackle",
|
|
||||||
"kcl",
|
"kcl",
|
||||||
"kcl-macros",
|
"kcl-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
kittycad = { version = "0.2.45", default-features = false, features = ["js"] }
|
kittycad = { version = "0.2.45", default-features = false, features = ["js"] }
|
||||||
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
|
||||||
kittycad-execution-plan-traits = "0.1.2"
|
|
||||||
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
|
||||||
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "executor"
|
name = "executor"
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "grackle"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
description = "A new executor for KCL which compiles to Execution Plans"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
kcl-lib = { path = "../kcl" }
|
|
||||||
kittycad-execution-plan = { workspace = true }
|
|
||||||
kittycad-execution-plan-traits = { workspace = true }
|
|
||||||
kittycad-modeling-session = { workspace = true }
|
|
||||||
thiserror = "1.0.56"
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_assertions = "1"
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
use kcl_lib::ast::types::LiteralIdentifier;
|
|
||||||
use kcl_lib::ast::types::LiteralValue;
|
|
||||||
|
|
||||||
use crate::CompileError;
|
|
||||||
use crate::KclFunction;
|
|
||||||
|
|
||||||
use super::native_functions;
|
|
||||||
use super::Address;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// KCL values which can be written to KCEP memory.
|
|
||||||
/// This is recursive. For example, the bound value might be an array, which itself contains bound values.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum EpBinding {
|
|
||||||
/// A KCL value which gets stored in a particular address in KCEP memory.
|
|
||||||
Single(Address),
|
|
||||||
/// A sequence of KCL values, indexed by their position in the sequence.
|
|
||||||
Sequence(Vec<EpBinding>),
|
|
||||||
/// A sequence of KCL values, indexed by their identifier.
|
|
||||||
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 {
|
|
||||||
/// Look up the given property of this binding.
|
|
||||||
pub fn property_of(&self, property: LiteralIdentifier) -> Result<&Self, CompileError> {
|
|
||||||
match property {
|
|
||||||
LiteralIdentifier::Identifier(_) => todo!("Support identifier properties"),
|
|
||||||
LiteralIdentifier::Literal(litval) => match litval.value {
|
|
||||||
// Arrays can be indexed by integers.
|
|
||||||
LiteralValue::IInteger(i) => match self {
|
|
||||||
EpBinding::Sequence(seq) => {
|
|
||||||
let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?;
|
|
||||||
seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() })
|
|
||||||
}
|
|
||||||
EpBinding::Map(_) => Err(CompileError::CannotIndex),
|
|
||||||
EpBinding::Single(_) => Err(CompileError::CannotIndex),
|
|
||||||
EpBinding::Function(_) => Err(CompileError::CannotIndex),
|
|
||||||
},
|
|
||||||
// Objects can be indexed by string properties.
|
|
||||||
LiteralValue::String(property) => match self {
|
|
||||||
EpBinding::Single(_) => Err(CompileError::NoProperties),
|
|
||||||
EpBinding::Function(_) => Err(CompileError::NoProperties),
|
|
||||||
EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties),
|
|
||||||
EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }),
|
|
||||||
},
|
|
||||||
// It's never valid to index by a fractional number.
|
|
||||||
LiteralValue::Fractional(num) => Err(CompileError::InvalidIndex(num.to_string())),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of bindings in a particular scope.
|
|
||||||
/// Bindings are KCL values that get "compiled" into KCEP values, which are stored in KCEP memory
|
|
||||||
/// at a particular KCEP address.
|
|
||||||
/// Bindings are referenced by the name of their KCL identifier.
|
|
||||||
///
|
|
||||||
/// KCL has multiple scopes -- each function has a scope for its own local variables and parameters.
|
|
||||||
/// So when referencing a variable, it might be in this scope, or the parent scope. So, each environment
|
|
||||||
/// has to keep track of parent environments. The root environment has no parent, and is used for KCL globals
|
|
||||||
/// (e.g. the prelude of stdlib functions).
|
|
||||||
///
|
|
||||||
/// These are called "Environments" in the "Crafting Interpreters" book.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BindingScope {
|
|
||||||
// KCL value which are stored in EP memory.
|
|
||||||
ep_bindings: HashMap<String, EpBinding>,
|
|
||||||
/// KCL functions. They do NOT get stored in EP memory.
|
|
||||||
parent: Option<Box<BindingScope>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BindingScope {
|
|
||||||
/// The parent scope for every program, before the user has defined anything.
|
|
||||||
/// Only includes some stdlib functions.
|
|
||||||
/// This is usually known as the "prelude" in other languages. It's the stdlib functions that
|
|
||||||
/// are already imported for you when you start coding.
|
|
||||||
pub fn prelude() -> Self {
|
|
||||||
Self {
|
|
||||||
// TODO: Actually put the stdlib prelude in here,
|
|
||||||
// things like `startSketchAt` and `line`.
|
|
||||||
ep_bindings: HashMap::from([
|
|
||||||
("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))),
|
|
||||||
("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
|
|
||||||
(
|
|
||||||
"startSketchAt".into(),
|
|
||||||
EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
parent: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new scope, e.g. for new function calls.
|
|
||||||
pub fn add_scope(&mut self) {
|
|
||||||
// Move all data from `self` into `this`.
|
|
||||||
let this_parent = self.parent.take();
|
|
||||||
let this_ep_bindings = self.ep_bindings.drain().collect();
|
|
||||||
let this = 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.
|
|
||||||
pub fn remove_scope(&mut self) {
|
|
||||||
// The scope is finished, so erase all its local variables.
|
|
||||||
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)
|
|
||||||
pub fn bind(&mut self, identifier: String, binding: EpBinding) {
|
|
||||||
self.ep_bindings.insert(identifier, binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Look up a binding.
|
|
||||||
pub fn get(&self, identifier: &str) -> Option<&EpBinding> {
|
|
||||||
if let Some(b) = self.ep_bindings.get(identifier) {
|
|
||||||
// The name was found in this scope.
|
|
||||||
Some(b)
|
|
||||||
} else if let Some(ref parent) = self.parent {
|
|
||||||
// Check the next scope outwards.
|
|
||||||
parent.get(identifier)
|
|
||||||
} else {
|
|
||||||
// There's no outer scope, and it wasn't found, so there's nowhere else to look.
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Look up a function bound to the given identifier.
|
|
||||||
pub fn get_fn(&self, identifier: &str) -> GetFnResult {
|
|
||||||
if let Some(x) = self.get(identifier) {
|
|
||||||
match x {
|
|
||||||
EpBinding::Function(f) => GetFnResult::Found(f),
|
|
||||||
_ => GetFnResult::NonCallable,
|
|
||||||
}
|
|
||||||
} else if let Some(ref parent) = self.parent {
|
|
||||||
parent.get_fn(identifier)
|
|
||||||
} else {
|
|
||||||
GetFnResult::NotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum GetFnResult<'a> {
|
|
||||||
Found(&'a KclFunction),
|
|
||||||
NonCallable,
|
|
||||||
NotFound,
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
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),
|
|
||||||
}
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
use kcl_lib::ast::{self, types::BinaryPart};
|
|
||||||
|
|
||||||
/// Basically the same enum as `kcl_lib::ast::types::Value`, but grouped according to whether the
|
|
||||||
/// value is singular or composite.
|
|
||||||
/// You can convert losslessly between KclValueGroup and `kcl_lib::ast::types::Value` with From/Into.
|
|
||||||
pub enum KclValueGroup {
|
|
||||||
Single(SingleValue),
|
|
||||||
ArrayExpression(Box<ast::types::ArrayExpression>),
|
|
||||||
ObjectExpression(Box<ast::types::ObjectExpression>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SingleValue {
|
|
||||||
Literal(Box<ast::types::Literal>),
|
|
||||||
Identifier(Box<ast::types::Identifier>),
|
|
||||||
BinaryExpression(Box<ast::types::BinaryExpression>),
|
|
||||||
CallExpression(Box<ast::types::CallExpression>),
|
|
||||||
PipeExpression(Box<ast::types::PipeExpression>),
|
|
||||||
UnaryExpression(Box<ast::types::UnaryExpression>),
|
|
||||||
KclNoneExpression(ast::types::KclNone),
|
|
||||||
MemberExpression(Box<ast::types::MemberExpression>),
|
|
||||||
FunctionExpression(Box<ast::types::FunctionExpression>),
|
|
||||||
PipeSubstitution(Box<ast::types::PipeSubstitution>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ast::types::BinaryPart> for KclValueGroup {
|
|
||||||
fn from(value: ast::types::BinaryPart) -> Self {
|
|
||||||
match value {
|
|
||||||
BinaryPart::Literal(e) => Self::Single(SingleValue::Literal(e)),
|
|
||||||
BinaryPart::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
|
|
||||||
BinaryPart::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
|
|
||||||
BinaryPart::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
|
|
||||||
BinaryPart::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
|
|
||||||
BinaryPart::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ast::types::BinaryPart> for SingleValue {
|
|
||||||
fn from(value: ast::types::BinaryPart) -> Self {
|
|
||||||
match value {
|
|
||||||
BinaryPart::Literal(e) => Self::Literal(e),
|
|
||||||
BinaryPart::Identifier(e) => Self::Identifier(e),
|
|
||||||
BinaryPart::BinaryExpression(e) => Self::BinaryExpression(e),
|
|
||||||
BinaryPart::CallExpression(e) => Self::CallExpression(e),
|
|
||||||
BinaryPart::UnaryExpression(e) => Self::UnaryExpression(e),
|
|
||||||
BinaryPart::MemberExpression(e) => Self::MemberExpression(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ast::types::Value> for KclValueGroup {
|
|
||||||
fn from(value: ast::types::Value) -> Self {
|
|
||||||
match value {
|
|
||||||
ast::types::Value::Literal(e) => Self::Single(SingleValue::Literal(e)),
|
|
||||||
ast::types::Value::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
|
|
||||||
ast::types::Value::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
|
|
||||||
ast::types::Value::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
|
|
||||||
ast::types::Value::PipeExpression(e) => Self::Single(SingleValue::PipeExpression(e)),
|
|
||||||
ast::types::Value::None(e) => Self::Single(SingleValue::KclNoneExpression(e)),
|
|
||||||
ast::types::Value::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
|
|
||||||
ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e),
|
|
||||||
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
|
|
||||||
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
|
|
||||||
ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
|
|
||||||
ast::types::Value::PipeSubstitution(e) => Self::Single(SingleValue::PipeSubstitution(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<KclValueGroup> for ast::types::Value {
|
|
||||||
fn from(value: KclValueGroup) -> Self {
|
|
||||||
match value {
|
|
||||||
KclValueGroup::Single(e) => match e {
|
|
||||||
SingleValue::Literal(e) => ast::types::Value::Literal(e),
|
|
||||||
SingleValue::Identifier(e) => ast::types::Value::Identifier(e),
|
|
||||||
SingleValue::BinaryExpression(e) => ast::types::Value::BinaryExpression(e),
|
|
||||||
SingleValue::CallExpression(e) => ast::types::Value::CallExpression(e),
|
|
||||||
SingleValue::PipeExpression(e) => ast::types::Value::PipeExpression(e),
|
|
||||||
SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e),
|
|
||||||
SingleValue::KclNoneExpression(e) => ast::types::Value::None(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::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,594 +0,0 @@
|
|||||||
mod binding_scope;
|
|
||||||
mod error;
|
|
||||||
mod kcl_value_group;
|
|
||||||
mod native_functions;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use kcl_lib::{
|
|
||||||
ast,
|
|
||||||
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
|
|
||||||
};
|
|
||||||
use kittycad_execution_plan as ep;
|
|
||||||
use kittycad_execution_plan::{Address, Instruction};
|
|
||||||
use kittycad_execution_plan_traits as ept;
|
|
||||||
use kittycad_execution_plan_traits::NumericPrimitive;
|
|
||||||
use kittycad_modeling_session::Session;
|
|
||||||
|
|
||||||
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
|
|
||||||
use self::error::{CompileError, Error};
|
|
||||||
use self::kcl_value_group::{KclValueGroup, SingleValue};
|
|
||||||
|
|
||||||
/// Execute a KCL program by compiling into an execution plan, then running that.
|
|
||||||
pub async fn execute(ast: Program, session: Session) -> Result<(), Error> {
|
|
||||||
let mut planner = Planner::new();
|
|
||||||
let (plan, _retval) = planner.build_plan(ast)?;
|
|
||||||
let mut mem = kittycad_execution_plan::Memory::default();
|
|
||||||
kittycad_execution_plan::execute(&mut mem, plan, session).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compiles KCL programs into Execution Plans.
|
|
||||||
struct Planner {
|
|
||||||
/// Maps KCL identifiers to what they hold, and where in KCEP virtual memory they'll be written to.
|
|
||||||
binding_scope: BindingScope,
|
|
||||||
/// Next available KCEP virtual machine memory address.
|
|
||||||
next_addr: Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Planner {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
binding_scope: BindingScope::prelude(),
|
|
||||||
next_addr: Address::ZERO,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If successful, return the KCEP instructions for executing the given program.
|
|
||||||
/// If the program is a function with a return, then it also returns the KCL function's return value.
|
|
||||||
fn build_plan(&mut self, program: Program) -> Result<(Vec<Instruction>, Option<EpBinding>), CompileError> {
|
|
||||||
program
|
|
||||||
.body
|
|
||||||
.into_iter()
|
|
||||||
.try_fold((Vec::new(), None), |(mut instructions, mut retval), item| {
|
|
||||||
if retval.is_some() {
|
|
||||||
return Err(CompileError::MultipleReturns);
|
|
||||||
}
|
|
||||||
let mut ctx = Context::default();
|
|
||||||
let instructions_for_this_node = match item {
|
|
||||||
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.
|
|
||||||
/// Returns the instructions, and the destination address of the value.
|
|
||||||
fn plan_to_compute_single(&mut self, ctx: &mut Context, value: SingleValue) -> Result<EvalPlan, CompileError> {
|
|
||||||
match value {
|
|
||||||
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
|
|
||||||
let address = self.next_addr.offset_by(1);
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: vec![Instruction::SetPrimitive {
|
|
||||||
address,
|
|
||||||
value: ept::Primitive::Nil,
|
|
||||||
}],
|
|
||||||
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) => {
|
|
||||||
let kcep_val = kcl_literal_to_kcep_literal(expr.value);
|
|
||||||
// KCEP primitives always have size of 1, because each address holds 1 primitive.
|
|
||||||
let size = 1;
|
|
||||||
let address = self.next_addr.offset_by(size);
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: vec![Instruction::SetPrimitive {
|
|
||||||
address,
|
|
||||||
value: kcep_val,
|
|
||||||
}],
|
|
||||||
binding: EpBinding::Single(address),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SingleValue::Identifier(expr) => {
|
|
||||||
// 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.
|
|
||||||
// Just return the address that it was stored at after being computed.
|
|
||||||
let previously_bound_to = self
|
|
||||||
.binding_scope
|
|
||||||
.get(&expr.name)
|
|
||||||
.ok_or(CompileError::Undefined { name: expr.name })?;
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: Vec::new(),
|
|
||||||
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) => {
|
|
||||||
let l = self.plan_to_compute_single(ctx, SingleValue::from(expr.left))?;
|
|
||||||
let r = self.plan_to_compute_single(ctx, SingleValue::from(expr.right))?;
|
|
||||||
let EpBinding::Single(l_binding) = l.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 EpBinding::Single(r_binding) = r.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 = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1);
|
|
||||||
plan.extend(l.instructions);
|
|
||||||
plan.extend(r.instructions);
|
|
||||||
plan.push(Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: match expr.operator {
|
|
||||||
ast::types::BinaryOperator::Add => ep::BinaryOperation::Add,
|
|
||||||
ast::types::BinaryOperator::Sub => ep::BinaryOperation::Sub,
|
|
||||||
ast::types::BinaryOperator::Mul => ep::BinaryOperation::Mul,
|
|
||||||
ast::types::BinaryOperator::Div => ep::BinaryOperation::Div,
|
|
||||||
ast::types::BinaryOperator::Mod => {
|
|
||||||
todo!("execution plan instruction set doesn't support Mod yet")
|
|
||||||
}
|
|
||||||
ast::types::BinaryOperator::Pow => {
|
|
||||||
todo!("execution plan instruction set doesn't support Pow yet")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
operand0: ep::Operand::Reference(l_binding),
|
|
||||||
operand1: ep::Operand::Reference(r_binding),
|
|
||||||
},
|
|
||||||
destination,
|
|
||||||
});
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: plan,
|
|
||||||
binding: EpBinding::Single(destination),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SingleValue::CallExpression(expr) => {
|
|
||||||
// Make a plan to compute all the arguments to this call.
|
|
||||||
let (mut instructions, args) = expr.arguments.into_iter().try_fold(
|
|
||||||
(Vec::new(), Vec::new()),
|
|
||||||
|(mut acc_instrs, mut acc_args), argument| {
|
|
||||||
let EvalPlan {
|
|
||||||
instructions: new_instructions,
|
|
||||||
binding: arg,
|
|
||||||
} = match KclValueGroup::from(argument) {
|
|
||||||
KclValueGroup::Single(value) => self.plan_to_compute_single(ctx, value)?,
|
|
||||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
|
||||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
|
||||||
};
|
|
||||||
acc_instrs.extend(new_instructions);
|
|
||||||
acc_args.push(arg);
|
|
||||||
Ok((acc_instrs, acc_args))
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
// Look up the function being called.
|
|
||||||
let callee = match self.binding_scope.get_fn(&expr.callee.name) {
|
|
||||||
GetFnResult::Found(f) => f,
|
|
||||||
GetFnResult::NonCallable => {
|
|
||||||
return Err(CompileError::NotCallable {
|
|
||||||
name: expr.callee.name.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
GetFnResult::NotFound => {
|
|
||||||
return Err(CompileError::Undefined {
|
|
||||||
name: expr.callee.name.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emit instructions to call that function with the given arguments.
|
|
||||||
use native_functions::Callable;
|
|
||||||
let EvalPlan {
|
|
||||||
instructions: eval_instrs,
|
|
||||||
binding,
|
|
||||||
} = 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);
|
|
||||||
Ok(EvalPlan { instructions, binding })
|
|
||||||
}
|
|
||||||
SingleValue::MemberExpression(mut expr) => {
|
|
||||||
let parse = move || {
|
|
||||||
let mut stack = Vec::new();
|
|
||||||
loop {
|
|
||||||
stack.push((expr.property, expr.computed));
|
|
||||||
match expr.object {
|
|
||||||
ast::types::MemberObject::MemberExpression(subexpr) => {
|
|
||||||
expr = subexpr;
|
|
||||||
}
|
|
||||||
ast::types::MemberObject::Identifier(id) => return (stack, id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (properties, id) = parse();
|
|
||||||
let name = id.name;
|
|
||||||
let mut binding = self.binding_scope.get(&name).ok_or(CompileError::Undefined { name })?;
|
|
||||||
for (property, computed) in properties {
|
|
||||||
if computed {
|
|
||||||
todo!("Support computed properties like '{:?}'", property);
|
|
||||||
} else {
|
|
||||||
binding = binding.property_of(property)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: Vec::new(),
|
|
||||||
binding: binding.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SingleValue::PipeSubstitution(_expr) => {
|
|
||||||
if let Some(ref binding) = ctx.pipe_substitution {
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: Vec::new(),
|
|
||||||
binding: binding.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(CompileError::NotInPipeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SingleValue::PipeExpression(expr) => {
|
|
||||||
let mut bodies = expr.body.into_iter();
|
|
||||||
|
|
||||||
// Get the first expression (i.e. body) of the pipeline.
|
|
||||||
let first = bodies.next().expect("Pipe expression must have > 1 item");
|
|
||||||
let EvalPlan {
|
|
||||||
mut instructions,
|
|
||||||
binding: mut current_value,
|
|
||||||
} = match KclValueGroup::from(first) {
|
|
||||||
KclValueGroup::Single(v) => self.plan_to_compute_single(ctx, v)?,
|
|
||||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
|
||||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle the remaining bodies.
|
|
||||||
for body in bodies {
|
|
||||||
let value = match KclValueGroup::from(body) {
|
|
||||||
KclValueGroup::Single(v) => v,
|
|
||||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
|
||||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
|
||||||
};
|
|
||||||
// This body will probably contain a % (pipe substitution character).
|
|
||||||
// So it needs to know what the previous pipeline body's value is,
|
|
||||||
// to replace the % with that value.
|
|
||||||
ctx.pipe_substitution = Some(current_value.clone());
|
|
||||||
let EvalPlan {
|
|
||||||
instructions: instructions_for_this_body,
|
|
||||||
binding,
|
|
||||||
} = self.plan_to_compute_single(ctx, value)?;
|
|
||||||
instructions.extend(instructions_for_this_body);
|
|
||||||
current_value = binding;
|
|
||||||
}
|
|
||||||
// Before we return, clear the pipe substitution, because nothing outside this
|
|
||||||
// pipeline should be able to use it anymore.
|
|
||||||
ctx.pipe_substitution = None;
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions,
|
|
||||||
binding: current_value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
|
|
||||||
/// Returns the instructions.
|
|
||||||
/// Also binds the value to a name.
|
|
||||||
fn plan_to_bind(
|
|
||||||
&mut self,
|
|
||||||
declarations: ast::types::VariableDeclaration,
|
|
||||||
) -> Result<Vec<Instruction>, CompileError> {
|
|
||||||
let mut ctx = Context::default();
|
|
||||||
declarations
|
|
||||||
.declarations
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(Vec::new(), |mut acc, declaration| {
|
|
||||||
let (instrs, binding) = self.plan_to_bind_one(&mut ctx, declaration.init)?;
|
|
||||||
self.binding_scope.bind(declaration.id.name, binding);
|
|
||||||
acc.extend(instrs);
|
|
||||||
Ok(acc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plan_to_bind_one(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut Context,
|
|
||||||
value_being_bound: ast::types::Value,
|
|
||||||
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
|
|
||||||
match KclValueGroup::from(value_being_bound) {
|
|
||||||
KclValueGroup::Single(init_value) => {
|
|
||||||
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
|
|
||||||
// and bind it to the KCL identifier.
|
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, init_value)?;
|
|
||||||
Ok((instructions, binding))
|
|
||||||
}
|
|
||||||
KclValueGroup::ArrayExpression(expr) => {
|
|
||||||
// First, emit a plan to compute each element of the array.
|
|
||||||
// Collect all the bindings from each element too.
|
|
||||||
let (instructions, bindings) = expr.elements.into_iter().try_fold(
|
|
||||||
(Vec::new(), Vec::new()),
|
|
||||||
|(mut acc_instrs, mut acc_bindings), element| {
|
|
||||||
match KclValueGroup::from(element) {
|
|
||||||
KclValueGroup::Single(value) => {
|
|
||||||
// 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.
|
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
acc_bindings.push(binding);
|
|
||||||
}
|
|
||||||
KclValueGroup::ArrayExpression(expr) => {
|
|
||||||
// If this element of the array is _itself_ an array, then we need to
|
|
||||||
// emit a plan to calculate each element of this child array.
|
|
||||||
// Then we collect the child array's bindings, and bind them to one
|
|
||||||
// element of the parent array.
|
|
||||||
let binding = expr
|
|
||||||
.elements
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(Vec::new(), |mut seq, child_element| {
|
|
||||||
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
seq.push(binding);
|
|
||||||
Ok(seq)
|
|
||||||
})
|
|
||||||
.map(EpBinding::Sequence)?;
|
|
||||||
acc_bindings.push(binding);
|
|
||||||
}
|
|
||||||
KclValueGroup::ObjectExpression(expr) => {
|
|
||||||
// If this element of the array is an object, then we need to
|
|
||||||
// emit a plan to calculate each value of each property of the object.
|
|
||||||
// Then we collect the bindings for each child value, and bind them to one
|
|
||||||
// element of the parent array.
|
|
||||||
let map = HashMap::with_capacity(expr.properties.len());
|
|
||||||
let binding = expr
|
|
||||||
.properties
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(map, |mut map, property| {
|
|
||||||
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
|
|
||||||
map.insert(property.key.name, binding);
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
Ok(map)
|
|
||||||
})
|
|
||||||
.map(EpBinding::Map)?;
|
|
||||||
acc_bindings.push(binding);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok((acc_instrs, acc_bindings))
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok((instructions, EpBinding::Sequence(bindings)))
|
|
||||||
}
|
|
||||||
KclValueGroup::ObjectExpression(expr) => {
|
|
||||||
// Convert the object to a sequence of key-value pairs.
|
|
||||||
let mut kvs = expr.properties.into_iter().map(|prop| (prop.key, prop.value));
|
|
||||||
let (instructions, each_property_binding) = kvs.try_fold(
|
|
||||||
(Vec::new(), HashMap::new()),
|
|
||||||
|(mut acc_instrs, mut acc_bindings), (key, value)| {
|
|
||||||
match KclValueGroup::from(value) {
|
|
||||||
KclValueGroup::Single(value) => {
|
|
||||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
acc_bindings.insert(key.name, binding);
|
|
||||||
}
|
|
||||||
KclValueGroup::ArrayExpression(expr) => {
|
|
||||||
// If this value of the object is an array, then emit a plan to calculate
|
|
||||||
// each element of that array. Collect their bindings, and bind them all
|
|
||||||
// under one property of the parent object.
|
|
||||||
let n = expr.elements.len();
|
|
||||||
let binding = expr
|
|
||||||
.elements
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
|
|
||||||
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
|
|
||||||
seq.push(binding);
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
Ok(seq)
|
|
||||||
})
|
|
||||||
.map(EpBinding::Sequence)?;
|
|
||||||
acc_bindings.insert(key.name, binding);
|
|
||||||
}
|
|
||||||
KclValueGroup::ObjectExpression(expr) => {
|
|
||||||
// If this value of the object is _itself_ an object, then we need to
|
|
||||||
// emit a plan to calculate each value of each property of the child object.
|
|
||||||
// Then we collect the bindings for each child value, and bind them to one
|
|
||||||
// property of the parent object.
|
|
||||||
let n = expr.properties.len();
|
|
||||||
let binding = expr
|
|
||||||
.properties
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(HashMap::with_capacity(n), |mut map, property| {
|
|
||||||
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
|
|
||||||
map.insert(property.key.name, binding);
|
|
||||||
acc_instrs.extend(instructions);
|
|
||||||
Ok(map)
|
|
||||||
})
|
|
||||||
.map(EpBinding::Map)?;
|
|
||||||
acc_bindings.insert(key.name, binding);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok((acc_instrs, acc_bindings))
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok((instructions, EpBinding::Map(each_property_binding)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
|
|
||||||
/// bound to some KCL name and Execution Plan address.
|
|
||||||
fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
|
|
||||||
match expr {
|
|
||||||
LiteralValue::IInteger(x) => ept::Primitive::NumericValue(NumericPrimitive::Integer(x)),
|
|
||||||
LiteralValue::Fractional(x) => ept::Primitive::NumericValue(NumericPrimitive::Float(x)),
|
|
||||||
LiteralValue::String(x) => ept::Primitive::String(x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instructions that can compute some value.
|
|
||||||
struct EvalPlan {
|
|
||||||
/// The instructions which will compute the value.
|
|
||||||
instructions: Vec<Instruction>,
|
|
||||||
/// Where the value will be stored.
|
|
||||||
binding: EpBinding,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>;
|
|
||||||
|
|
||||||
#[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>,
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
//! Defines functions which are written in Rust, but called from KCL.
|
|
||||||
//! This includes some of the stdlib, e.g. `startSketchAt`.
|
|
||||||
//! But some other stdlib functions will be written in KCL.
|
|
||||||
|
|
||||||
use kcl_lib::std::sketch::PlaneData;
|
|
||||||
use kittycad_execution_plan::{Address, BinaryArithmetic, Instruction};
|
|
||||||
use kittycad_execution_plan_traits::Value;
|
|
||||||
|
|
||||||
use crate::{CompileError, EpBinding, EvalPlan};
|
|
||||||
|
|
||||||
/// The identity function. Always returns its first input.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct 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> {
|
|
||||||
if args.len() > 1 {
|
|
||||||
return Err(CompileError::TooManyArgs {
|
|
||||||
fn_name: "id".into(),
|
|
||||||
maximum: 1,
|
|
||||||
actual: args.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let arg = args
|
|
||||||
.first()
|
|
||||||
.ok_or(CompileError::NotEnoughArgs {
|
|
||||||
fn_name: "id".into(),
|
|
||||||
required: 1,
|
|
||||||
actual: 0,
|
|
||||||
})?
|
|
||||||
.clone();
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: Vec::new(),
|
|
||||||
binding: arg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct StartSketchAt;
|
|
||||||
|
|
||||||
impl Callable for StartSketchAt {
|
|
||||||
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
|
||||||
let mut instructions = Vec::new();
|
|
||||||
// Store the plane.
|
|
||||||
let plane = PlaneData::XY.into_parts();
|
|
||||||
instructions.push(Instruction::SetValue {
|
|
||||||
address: next_addr.offset_by(plane.len()),
|
|
||||||
value_parts: plane,
|
|
||||||
});
|
|
||||||
// TODO: Get the plane ID from global context.
|
|
||||||
// TODO: Send this command:
|
|
||||||
// ModelingCmd::SketchModeEnable {
|
|
||||||
// animated: false,
|
|
||||||
// ortho: false,
|
|
||||||
// plane_id: plane.id,
|
|
||||||
// // We pass in the normal for the plane here.
|
|
||||||
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
|
|
||||||
// },
|
|
||||||
// TODO: Send ModelingCmd::StartPath at the given point.
|
|
||||||
// TODO (maybe): Store the SketchGroup in KCEP memory.
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A test function that adds two numbers.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Add;
|
|
||||||
|
|
||||||
impl Callable for Add {
|
|
||||||
fn call(&self, next_address: &mut Address, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
|
||||||
let len = args.len();
|
|
||||||
if len > 2 {
|
|
||||||
return Err(CompileError::TooManyArgs {
|
|
||||||
fn_name: "add".into(),
|
|
||||||
maximum: 2,
|
|
||||||
actual: len,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let not_enough_args = CompileError::NotEnoughArgs {
|
|
||||||
fn_name: "add".into(),
|
|
||||||
required: 2,
|
|
||||||
actual: len,
|
|
||||||
};
|
|
||||||
const ERR: &str = "cannot use composite values (e.g. array) as arguments to Add";
|
|
||||||
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
|
|
||||||
return Err(CompileError::InvalidOperand(ERR));
|
|
||||||
};
|
|
||||||
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args)? else {
|
|
||||||
return Err(CompileError::InvalidOperand(ERR));
|
|
||||||
};
|
|
||||||
let destination = next_address.offset_by(1);
|
|
||||||
Ok(EvalPlan {
|
|
||||||
instructions: vec![Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: BinaryArithmetic {
|
|
||||||
operation: kittycad_execution_plan::BinaryOperation::Add,
|
|
||||||
operand0: kittycad_execution_plan::Operand::Reference(arg0),
|
|
||||||
operand1: kittycad_execution_plan::Operand::Reference(arg1),
|
|
||||||
},
|
|
||||||
destination,
|
|
||||||
}],
|
|
||||||
binding: EpBinding::Single(destination),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,745 +0,0 @@
|
|||||||
use ep::UnaryArithmetic;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn must_plan(program: &str) -> (Vec<Instruction>, BindingScope) {
|
|
||||||
let tokens = kcl_lib::token::lexer(program);
|
|
||||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
|
||||||
let ast = parser.ast().unwrap();
|
|
||||||
let mut p = Planner::new();
|
|
||||||
let (instrs, _) = p.build_plan(ast).unwrap();
|
|
||||||
(instrs, p.binding_scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_not_compile(program: &str) -> CompileError {
|
|
||||||
let tokens = kcl_lib::token::lexer(program);
|
|
||||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
|
||||||
let ast = parser.ast().unwrap();
|
|
||||||
let mut p = Planner::new();
|
|
||||||
p.build_plan(ast).unwrap_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn assignments() {
|
|
||||||
let program = "
|
|
||||||
let x = 1
|
|
||||||
let y = 2";
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 2i64.into(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bind_array() {
|
|
||||||
let program = r#"let x = [44, 55, "sixty-six"]"#;
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 44i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 55i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(2),
|
|
||||||
value: "sixty-six".to_owned().into(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bind_nested_array() {
|
|
||||||
let program = r#"let x = [44, [55, "sixty-six"]]"#;
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 44i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 55i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(2),
|
|
||||||
value: "sixty-six".to_owned().into(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bind_arrays_with_objects_elements() {
|
|
||||||
let program = r#"let x = [44, {a: 55, b: "sixty-six"}]"#;
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 44i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 55i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(2),
|
|
||||||
value: "sixty-six".to_owned().into(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
|
||||||
fn name_not_found() {
|
|
||||||
// Users can't assign `y` to anything because `y` is undefined.
|
|
||||||
let err = should_not_compile("let x = y");
|
|
||||||
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]
|
|
||||||
fn aliases() {
|
|
||||||
let program = "
|
|
||||||
let x = 1
|
|
||||||
let y = x";
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into(),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn use_native_function_add() {
|
|
||||||
let program = "let x = add(1,2)";
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into()
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 2i64.into()
|
|
||||||
},
|
|
||||||
Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: ep::BinaryOperation::Add,
|
|
||||||
operand0: ep::Operand::Reference(Address::ZERO),
|
|
||||||
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
|
|
||||||
},
|
|
||||||
destination: Address::ZERO.offset(2),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn use_native_function_id() {
|
|
||||||
let program = "let x = id(2)";
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 2i64.into()
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "haven't done computed properties yet"]
|
|
||||||
fn computed_array_index() {
|
|
||||||
let program = r#"
|
|
||||||
let array = ["a", "b", "c"]
|
|
||||||
let index = 1+1
|
|
||||||
let prop = array[index]
|
|
||||||
"#;
|
|
||||||
let (_plan, scope) = must_plan(program);
|
|
||||||
match scope.get("prop").unwrap() {
|
|
||||||
EpBinding::Single(addr) => {
|
|
||||||
assert_eq!(*addr, Address::ZERO + 1);
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "haven't done computed properties yet"]
|
|
||||||
fn computed_member_expressions() {
|
|
||||||
let program = r#"
|
|
||||||
let obj = {x: 1, y: 2}
|
|
||||||
let index = "x"
|
|
||||||
let prop = obj[index]
|
|
||||||
"#;
|
|
||||||
let (_plan, scope) = must_plan(program);
|
|
||||||
match scope.get("prop").unwrap() {
|
|
||||||
EpBinding::Single(addr) => {
|
|
||||||
assert_eq!(*addr, Address::ZERO + 1);
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn member_expressions_object() {
|
|
||||||
let program = r#"
|
|
||||||
let obj = {x: 1, y: 2}
|
|
||||||
let prop = obj["y"]
|
|
||||||
"#;
|
|
||||||
let (_plan, scope) = must_plan(program);
|
|
||||||
match scope.get("prop").unwrap() {
|
|
||||||
EpBinding::Single(addr) => {
|
|
||||||
assert_eq!(*addr, Address::ZERO + 1);
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn member_expressions_array() {
|
|
||||||
let program = "
|
|
||||||
let array = [[1,2],[3,4]]
|
|
||||||
let first = array[0][0]
|
|
||||||
let last = array[1][1]
|
|
||||||
";
|
|
||||||
let (_plan, scope) = must_plan(program);
|
|
||||||
match scope.get("first").unwrap() {
|
|
||||||
EpBinding::Single(addr) => {
|
|
||||||
assert_eq!(*addr, Address::ZERO);
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!("expected 'number' bound to 0x0 but it was bound to {other:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match scope.get("last").unwrap() {
|
|
||||||
EpBinding::Single(addr) => {
|
|
||||||
assert_eq!(*addr, Address::ZERO + 3);
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!("expected 'number' bound to 0x3 but it was bound to {other:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
|
||||||
fn add_literals() {
|
|
||||||
let program = "let x = 1 + 2";
|
|
||||||
let (plan, _scope) = must_plan(program);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into()
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 2i64.into()
|
|
||||||
},
|
|
||||||
Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: ep::BinaryOperation::Add,
|
|
||||||
operand0: ep::Operand::Reference(Address::ZERO),
|
|
||||||
operand1: ep::Operand::Reference(Address::ZERO.offset(1)),
|
|
||||||
},
|
|
||||||
destination: Address::ZERO.offset(2),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_vars() {
|
|
||||||
let program = "
|
|
||||||
let one = 1
|
|
||||||
let two = 2
|
|
||||||
let x = one + two";
|
|
||||||
let (plan, _bindings) = must_plan(program);
|
|
||||||
let addr0 = Address::ZERO;
|
|
||||||
let addr1 = Address::ZERO.offset(1);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: addr0,
|
|
||||||
value: 1i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: addr1,
|
|
||||||
value: 2i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: ep::BinaryOperation::Add,
|
|
||||||
operand0: ep::Operand::Reference(addr0),
|
|
||||||
operand1: ep::Operand::Reference(addr1),
|
|
||||||
},
|
|
||||||
destination: Address::ZERO.offset(2),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn composite_binary_exprs() {
|
|
||||||
let program = "
|
|
||||||
let x = 1
|
|
||||||
let y = 2
|
|
||||||
let z = 3
|
|
||||||
let six = x + y + z
|
|
||||||
";
|
|
||||||
let (plan, _bindings) = must_plan(program);
|
|
||||||
let addr0 = Address::ZERO;
|
|
||||||
let addr1 = Address::ZERO.offset(1);
|
|
||||||
let addr2 = Address::ZERO.offset(2);
|
|
||||||
let addr3 = Address::ZERO.offset(3);
|
|
||||||
assert_eq!(
|
|
||||||
plan,
|
|
||||||
vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: addr0,
|
|
||||||
value: 1i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: addr1,
|
|
||||||
value: 2i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: addr2,
|
|
||||||
value: 3i64.into(),
|
|
||||||
},
|
|
||||||
// Adds 1 + 2
|
|
||||||
Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: ep::BinaryOperation::Add,
|
|
||||||
operand0: ep::Operand::Reference(addr0),
|
|
||||||
operand1: ep::Operand::Reference(addr1),
|
|
||||||
},
|
|
||||||
destination: addr3,
|
|
||||||
},
|
|
||||||
// Adds `x` + 3, where `x` is (1 + 2)
|
|
||||||
Instruction::BinaryArithmetic {
|
|
||||||
arithmetic: ep::BinaryArithmetic {
|
|
||||||
operation: ep::BinaryOperation::Add,
|
|
||||||
operand0: ep::Operand::Reference(addr3),
|
|
||||||
operand1: ep::Operand::Reference(addr2),
|
|
||||||
},
|
|
||||||
destination: Address::ZERO.offset(4),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
|
||||||
fn aliases_dont_affect_plans() {
|
|
||||||
let (plan1, _) = must_plan(
|
|
||||||
"let one = 1
|
|
||||||
let two = 2
|
|
||||||
let x = one + two",
|
|
||||||
);
|
|
||||||
let (plan2, _) = must_plan(
|
|
||||||
"let one = 1
|
|
||||||
let two = 2
|
|
||||||
let y = two
|
|
||||||
let x = one + y",
|
|
||||||
);
|
|
||||||
assert_eq!(plan1, plan2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn store_object() {
|
|
||||||
let program = "const x0 = {a: 1, b: 2, c: {d: 3}}";
|
|
||||||
let (actual, bindings) = must_plan(program);
|
|
||||||
let expected = vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 2i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(2),
|
|
||||||
value: 3i64.into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
eprintln!("{bindings:#?}");
|
|
||||||
assert_eq!(
|
|
||||||
bindings.get("x0").unwrap(),
|
|
||||||
&EpBinding::Map(HashMap::from([
|
|
||||||
("a".to_owned(), EpBinding::Single(Address::ZERO),),
|
|
||||||
("b".to_owned(), EpBinding::Single(Address::ZERO.offset(1))),
|
|
||||||
(
|
|
||||||
"c".to_owned(),
|
|
||||||
EpBinding::Map(HashMap::from([(
|
|
||||||
"d".to_owned(),
|
|
||||||
EpBinding::Single(Address::ZERO.offset(2))
|
|
||||||
)]))
|
|
||||||
),
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn store_object_with_array_property() {
|
|
||||||
let program = "const x0 = {a: 1, b: [2, 3]}";
|
|
||||||
let (actual, bindings) = must_plan(program);
|
|
||||||
let expected = vec![
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO,
|
|
||||||
value: 1i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(1),
|
|
||||||
value: 2i64.into(),
|
|
||||||
},
|
|
||||||
Instruction::SetPrimitive {
|
|
||||||
address: Address::ZERO.offset(2),
|
|
||||||
value: 3i64.into(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
eprintln!("{bindings:#?}");
|
|
||||||
assert_eq!(
|
|
||||||
bindings.get("x0").unwrap(),
|
|
||||||
&EpBinding::Map(HashMap::from([
|
|
||||||
("a".to_owned(), EpBinding::Single(Address::ZERO),),
|
|
||||||
(
|
|
||||||
"b".to_owned(),
|
|
||||||
EpBinding::Sequence(vec![
|
|
||||||
EpBinding::Single(Address::ZERO.offset(1)),
|
|
||||||
EpBinding::Single(Address::ZERO.offset(2)),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ignore = "haven't done API calls or stdlib yet"]
|
|
||||||
#[test]
|
|
||||||
fn stdlib_api_calls() {
|
|
||||||
let program = "const x0 = startSketchAt([0, 0])
|
|
||||||
const x1 = line([0, 10], x0)
|
|
||||||
const x2 = line([10, 0], x1)
|
|
||||||
const x3 = line([0, -10], x2)
|
|
||||||
const x4 = line([0, 0], x3)
|
|
||||||
const x5 = close(x4)
|
|
||||||
const x6 = extrude(20, x5)
|
|
||||||
show(x6)";
|
|
||||||
must_plan(program);
|
|
||||||
}
|
|
||||||
@ -21,18 +21,15 @@ databake = { version = "0.1.7", features = ["derive"] }
|
|||||||
derive-docs = { version = "0.1.5" }
|
derive-docs = { version = "0.1.5" }
|
||||||
# derive-docs = { path = "../derive-docs" }
|
# derive-docs = { path = "../derive-docs" }
|
||||||
kittycad = { workspace = true }
|
kittycad = { workspace = true }
|
||||||
kittycad-execution-plan-macros = { workspace = true }
|
|
||||||
kittycad-execution-plan-traits = { workspace = true }
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
parse-display = "0.8.2"
|
parse-display = "0.8.2"
|
||||||
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
ts-rs = { version = "7", features = ["uuid-impl"] }
|
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
|
||||||
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
||||||
winnow = "0.5.18"
|
winnow = "0.5.18"
|
||||||
either = "1.6.1"
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
js-sys = { version = "0.3.65" }
|
js-sys = { version = "0.3.65" }
|
||||||
|
|||||||
@ -19,7 +19,6 @@ 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;
|
||||||
@ -1434,7 +1433,6 @@ 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![],
|
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1446,7 +1444,6 @@ 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![],
|
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1644,7 +1641,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, vec![]).await?,
|
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).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),
|
||||||
@ -1668,7 +1665,6 @@ 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![],
|
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1798,7 +1794,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, vec![]).await?,
|
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).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),
|
||||||
@ -1826,7 +1822,6 @@ 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![],
|
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2036,7 +2031,6 @@ 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 {
|
||||||
@ -2093,7 +2087,6 @@ 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 {
|
||||||
@ -2258,7 +2251,6 @@ impl BinaryExpression {
|
|||||||
value,
|
value,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
path_to_node: vec![],
|
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -2280,7 +2272,6 @@ impl BinaryExpression {
|
|||||||
value,
|
value,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
path_to_node: vec![],
|
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2444,7 +2435,6 @@ 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![],
|
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -2574,12 +2564,11 @@ 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, path_to_node).await
|
execute_pipe_body(memory, &self.body, pipe_info, self.into(), ctx).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.
|
||||||
@ -2597,8 +2586,6 @@ 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;
|
||||||
@ -2644,7 +2631,7 @@ async fn execute_pipe_body(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parameter of a KCL function.
|
/// Parameter of a KCL function.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema, Bake)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, 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")]
|
||||||
@ -2668,23 +2655,6 @@ 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 {
|
||||||
@ -2693,36 +2663,6 @@ 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] {
|
||||||
|
|||||||
@ -42,7 +42,6 @@ pub struct StdLibFnArg {
|
|||||||
/// The type of the argument.
|
/// The type of the argument.
|
||||||
pub type_: String,
|
pub type_: String,
|
||||||
/// The schema of the argument.
|
/// The schema of the argument.
|
||||||
#[ts(type = "any")]
|
|
||||||
pub schema: schemars::schema::Schema,
|
pub schema: schemars::schema::Schema,
|
||||||
/// If the argument is required.
|
/// If the argument is required.
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
|
|||||||
@ -5,13 +5,11 @@ use std::{collections::HashMap, sync::Arc};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use kittycad::types::{Color, ModelingCmd, Point3D};
|
use kittycad::types::{Color, ModelingCmd, Point3D};
|
||||||
use kittycad_execution_plan_macros::ExecutionPlanValue;
|
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
use schemars::JsonSchema;
|
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},
|
||||||
@ -288,7 +286,6 @@ impl DefaultPlanes {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
pub struct UserVal {
|
pub struct UserVal {
|
||||||
#[ts(type = "any")]
|
|
||||||
pub value: serde_json::Value,
|
pub value: serde_json::Value,
|
||||||
#[serde(rename = "__meta")]
|
#[serde(rename = "__meta")]
|
||||||
pub meta: Vec<Metadata>,
|
pub meta: Vec<Metadata>,
|
||||||
@ -613,7 +610,7 @@ impl Point2d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
|
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct Point3d {
|
pub struct Point3d {
|
||||||
pub x: f64,
|
pub x: f64,
|
||||||
@ -633,18 +630,6 @@ 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)]
|
||||||
@ -652,16 +637,11 @@ pub type PathToNode = Vec<(NumberOrString, String)>;
|
|||||||
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 {
|
Self { source_range }
|
||||||
source_range,
|
|
||||||
path_to_node: Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -847,17 +827,12 @@ 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 (index, statement) in program.body.iter().enumerate() {
|
for statement in &program.body {
|
||||||
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, with_body_path_to_node).await?;
|
pipe_expr.get_result(memory, &mut pipe_info, ctx).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();
|
||||||
@ -928,15 +903,10 @@ 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 mut with_dec_path_to_node = with_body_path_to_node.clone();
|
let metadata = Metadata { source_range };
|
||||||
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) => {
|
||||||
@ -991,7 +961,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, with_dec_path_to_node).await?;
|
let result = pipe_expression.get_result(memory, &mut pipe_info, ctx).await?;
|
||||||
memory.add(&var_name, result, source_range)?;
|
memory.add(&var_name, result, source_range)?;
|
||||||
}
|
}
|
||||||
Value::PipeSubstitution(pipe_substitution) => {
|
Value::PipeSubstitution(pipe_substitution) => {
|
||||||
@ -1055,7 +1025,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, with_body_path_to_node).await?;
|
let result = pipe_expr.get_result(memory, &mut pipe_info, ctx).await?;
|
||||||
memory.return_ = Some(ProgramReturn::Value(result));
|
memory.return_ = Some(ProgramReturn::Value(result));
|
||||||
}
|
}
|
||||||
Value::PipeSubstitution(_) => {}
|
Value::PipeSubstitution(_) => {}
|
||||||
|
|||||||
@ -2721,7 +2721,7 @@ show(b1)
|
|||||||
show(b2)"#;
|
show(b2)"#;
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
dbg!(parser.ast().unwrap());
|
parser.ast().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -190,7 +190,6 @@ 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()
|
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use derive_docs::stdlib;
|
use derive_docs::stdlib;
|
||||||
use kittycad::types::{Angle, ModelingCmd, Point3D};
|
use kittycad::types::{Angle, ModelingCmd, Point3D};
|
||||||
use kittycad_execution_plan_macros::ExecutionPlanValue;
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -649,7 +648,7 @@ async fn inner_start_sketch_at(data: LineData, args: Args) -> Result<Box<SketchG
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Data for a plane.
|
/// Data for a plane.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum PlaneData {
|
pub enum PlaneData {
|
||||||
|
|||||||
@ -237,7 +237,6 @@ async fn serial_test_execute_cylinder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[ignore = "currently stack overflows"]
|
|
||||||
async fn serial_test_execute_kittycad_svg() {
|
async fn serial_test_execute_kittycad_svg() {
|
||||||
let code = include_str!("inputs/kittycad_svg.kcl");
|
let code = include_str!("inputs/kittycad_svg.kcl");
|
||||||
|
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@ -4590,10 +4590,15 @@ flux@^4.0.1:
|
|||||||
fbemitter "^3.0.0"
|
fbemitter "^3.0.0"
|
||||||
fbjs "^3.0.1"
|
fbjs "^3.0.1"
|
||||||
|
|
||||||
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
|
follow-redirects@^1.0.0, follow-redirects@^1.14.8:
|
||||||
version "1.15.4"
|
version "1.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.0:
|
||||||
|
version "1.15.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
|
||||||
|
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
|
||||||
|
|
||||||
for-each@^0.3.3:
|
for-each@^0.3.3:
|
||||||
version "0.3.3"
|
version "0.3.3"
|
||||||
@ -8188,10 +8193,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.2:
|
"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:
|
||||||
version "4.5.2"
|
version "4.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a"
|
||||||
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
|
integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.18.10"
|
esbuild "^0.18.10"
|
||||||
postcss "^8.4.27"
|
postcss "^8.4.27"
|
||||||
|
|||||||
Reference in New Issue
Block a user