mirror of
https://github.com/enso-org/enso.git
synced 2024-12-29 18:23:25 +03:00
Merge branch 'develop' into wip/gmt/8110-map-error
This commit is contained in:
commit
f8f0b2f597
@ -179,6 +179,14 @@ export interface MethodPointer {
|
||||
name: string
|
||||
}
|
||||
|
||||
export function methodPointerEquals(left: MethodPointer, right: MethodPointer): boolean {
|
||||
return (
|
||||
left.module === right.module &&
|
||||
left.definedOnType === right.definedOnType &&
|
||||
left.name === right.name
|
||||
)
|
||||
}
|
||||
|
||||
export type ProfilingInfo = ExecutionTime
|
||||
|
||||
export interface ExecutionTime {
|
||||
@ -353,6 +361,18 @@ export interface LocalCall {
|
||||
expressionId: ExpressionId
|
||||
}
|
||||
|
||||
export function stackItemsEqual(left: StackItem, right: StackItem): boolean {
|
||||
if (left.type !== right.type) return false
|
||||
|
||||
if (left.type === 'ExplicitCall') {
|
||||
const explicitRight = right as ExplicitCall
|
||||
return methodPointerEquals(left.methodPointer, explicitRight.methodPointer)
|
||||
} else {
|
||||
const localRight = right as LocalCall
|
||||
return left.expressionId === localRight.expressionId
|
||||
}
|
||||
}
|
||||
|
||||
export namespace response {
|
||||
export interface OpenTextFile {
|
||||
writeCapability: CapabilityRegistration | null
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { bail } from '@/util/assert'
|
||||
import { MultiRange, Range } from '@/util/range'
|
||||
import { Rect } from '@/util/rect'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
|
||||
@ -42,23 +41,12 @@ export function nonDictatedPlacement(
|
||||
const initialRect = new Rect(initialPosition, nodeSize)
|
||||
let top = initialPosition.y
|
||||
const height = nodeSize.y
|
||||
const minimumVerticalSpace = height + gap * 2
|
||||
const bottom = () => top + height
|
||||
const occupiedYRanges = new MultiRange()
|
||||
for (const rect of nodeRects) {
|
||||
const nodeRectsSorted = Array.from(nodeRects).sort((a, b) => a.top - b.top)
|
||||
for (const rect of nodeRectsSorted) {
|
||||
if (initialRect.intersectsX(rect) && rect.bottom + gap > top) {
|
||||
if (rect.top - bottom() >= gap) {
|
||||
const range = new Range(rect.top, rect.bottom)
|
||||
occupiedYRanges.insert(range, range.expand(gap))
|
||||
} else {
|
||||
if (rect.top - bottom() < gap) {
|
||||
top = rect.bottom + gap
|
||||
const rangeIncludingTop = occupiedYRanges
|
||||
.remove(new Range(-Infinity, rect.bottom + minimumVerticalSpace))
|
||||
.at(-1)
|
||||
if (rangeIncludingTop) {
|
||||
top = Math.max(top, rangeIncludingTop.end + gap)
|
||||
occupiedYRanges.remove(rangeIncludingTop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,24 +93,13 @@ export function previousNodeDictatedPlacement(
|
||||
let left = initialLeft
|
||||
const width = nodeSize.x
|
||||
const right = () => left + width
|
||||
const minimumHorizontalSpace = width + gap * 2
|
||||
const initialPosition = new Vec2(left, top)
|
||||
const initialRect = new Rect(initialPosition, nodeSize)
|
||||
const occupiedXRanges = new MultiRange()
|
||||
for (const rect of nodeRects) {
|
||||
const sortedNodeRects = Array.from(nodeRects).sort((a, b) => a.left - b.left)
|
||||
for (const rect of sortedNodeRects) {
|
||||
if (initialRect.intersectsY(rect) && rect.right + gap > left) {
|
||||
if (rect.left - right() >= gap) {
|
||||
const range = new Range(rect.left, rect.right)
|
||||
occupiedXRanges.insert(range, range.expand(gap))
|
||||
} else {
|
||||
if (rect.left - right() < gap) {
|
||||
left = rect.right + gap
|
||||
const rangeIncludingLeft = occupiedXRanges
|
||||
.remove(new Range(-Infinity, rect.right + minimumHorizontalSpace))
|
||||
.at(-1)
|
||||
if (rangeIncludingLeft) {
|
||||
left = Math.max(left, rangeIncludingLeft.end + gap)
|
||||
occupiedXRanges.remove(rangeIncludingLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,19 +244,26 @@ function startNodeCreation() {
|
||||
}
|
||||
|
||||
async function handleFileDrop(event: DragEvent) {
|
||||
// A vertical gap between created nodes when multiple files were dropped together.
|
||||
const MULTIPLE_FILES_GAP = 50
|
||||
|
||||
try {
|
||||
if (event.dataTransfer && event.dataTransfer.items) {
|
||||
;[...event.dataTransfer.items].forEach(async (item) => {
|
||||
;[...event.dataTransfer.items].forEach(async (item, index) => {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
const clientPos = new Vec2(event.clientX, event.clientY)
|
||||
const pos = navigator.clientToScenePos(clientPos)
|
||||
const uploader = await Uploader.create(
|
||||
const offset = new Vec2(0, index * -MULTIPLE_FILES_GAP)
|
||||
const pos = navigator.clientToScenePos(clientPos).add(offset)
|
||||
const uploader = await Uploader.Create(
|
||||
projectStore.lsRpcConnection,
|
||||
projectStore.dataConnection,
|
||||
projectStore.contentRoots,
|
||||
projectStore.awareness,
|
||||
file,
|
||||
pos,
|
||||
projectStore.executionContext.getStackTop(),
|
||||
)
|
||||
const name = await uploader.upload()
|
||||
graphStore.createNode(pos, uploadedExpression(name))
|
||||
|
@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import GraphNode from '@/components/GraphEditor/GraphNode.vue'
|
||||
import UploadingFile from '@/components/GraphEditor/UploadingFile.vue'
|
||||
import { useDragging } from '@/components/GraphEditor/dragging'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||
import type { UploadingFile as File, FileName } from '@/stores/awareness'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import type { Vec2 } from '@/util/vec2'
|
||||
import { stackItemsEqual } from 'shared/languageServerTypes'
|
||||
import type { ContentRange, ExprId } from 'shared/yjsModel'
|
||||
import { computed, toRaw } from 'vue'
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const graphStore = useGraphStore()
|
||||
const dragging = useDragging()
|
||||
const selection = injectGraphSelection(true)
|
||||
@ -28,6 +34,13 @@ function nodeIsDragged(movedId: ExprId, offset: Vec2) {
|
||||
function hoverNode(id: ExprId | undefined) {
|
||||
if (selection != null) selection.hoveredNode = id
|
||||
}
|
||||
|
||||
const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||
const currentStackItem = projectStore.executionContext.getStackTop()
|
||||
return [...projectStore.awareness.allUploads()].filter(([_name, file]) =>
|
||||
stackItemsEqual(file.stackItem, toRaw(currentStackItem)),
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -50,4 +63,10 @@ function hoverNode(id: ExprId | undefined) {
|
||||
@draggingCommited="dragging.finishDrag()"
|
||||
@outputPortAction="graphStore.createEdgeFromOutput(id)"
|
||||
/>
|
||||
<UploadingFile
|
||||
v-for="(nameAndFile, index) in uploadingFiles"
|
||||
:key="index"
|
||||
:name="nameAndFile[0]"
|
||||
:file="nameAndFile[1]"
|
||||
/>
|
||||
</template>
|
||||
|
42
app/gui2/src/components/GraphEditor/UploadingFile.vue
Normal file
42
app/gui2/src/components/GraphEditor/UploadingFile.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadingFile } from '@/stores/awareness'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
file: UploadingFile
|
||||
}>()
|
||||
|
||||
const transform = computed(() => {
|
||||
let pos = props.file.position
|
||||
return `translate(${pos.x}px, ${pos.y}px)`
|
||||
})
|
||||
|
||||
const backgroundOffset = computed(() => 200 - props.file.sizePercentage)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="UploadingFile"
|
||||
:style="{ transform, 'background-position': `${backgroundOffset}% 0` }"
|
||||
>
|
||||
<span>{{ `Uploading ${props.name} (${props.file.sizePercentage}%)` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.UploadingFile {
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 4px 8px;
|
||||
z-index: 2;
|
||||
outline: 0px solid transparent;
|
||||
background: linear-gradient(to right, #e0e0e0 0%, #e0e0e0 50%, #ffffff 50%, #ffffff 100%);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
</style>
|
@ -1,14 +1,21 @@
|
||||
import { Awareness, type UploadingFile } from '@/stores/awareness'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import { Keccak, sha3_224 as SHA3 } from '@noble/hashes/sha3'
|
||||
import type { Hash } from '@noble/hashes/utils'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import type { DataServer } from 'shared/dataServer'
|
||||
import type { LanguageServer } from 'shared/languageServer'
|
||||
import { ErrorCode, RemoteRpcError } from 'shared/languageServer'
|
||||
import type { ContentRoot, Path, Uuid } from 'shared/languageServerTypes'
|
||||
import type { ContentRoot, Path, StackItem, Uuid } from 'shared/languageServerTypes'
|
||||
import { markRaw, toRaw } from 'vue'
|
||||
|
||||
// === Constants ===
|
||||
|
||||
export const uploadedExpression = (name: string) => `enso_project.data/"${name}" . read`
|
||||
const DATA_DIR_NAME = 'data'
|
||||
|
||||
// === Uploader ===
|
||||
|
||||
export class Uploader {
|
||||
private rpc: LanguageServer
|
||||
private binary: DataServer
|
||||
@ -16,46 +23,88 @@ export class Uploader {
|
||||
private projectRootId: Uuid
|
||||
private checksum: Hash<Keccak>
|
||||
private uploadedBytes: bigint
|
||||
private awareness: Awareness
|
||||
private position: Vec2
|
||||
private stackItem: StackItem
|
||||
|
||||
private constructor(rpc: LanguageServer, binary: DataServer, file: File, projectRootId: Uuid) {
|
||||
private constructor(
|
||||
rpc: LanguageServer,
|
||||
binary: DataServer,
|
||||
awareness: Awareness,
|
||||
file: File,
|
||||
projectRootId: Uuid,
|
||||
position: Vec2,
|
||||
stackItem: StackItem,
|
||||
) {
|
||||
this.rpc = rpc
|
||||
this.binary = binary
|
||||
this.awareness = awareness
|
||||
this.file = file
|
||||
this.projectRootId = projectRootId
|
||||
this.checksum = SHA3.create()
|
||||
this.uploadedBytes = BigInt(0)
|
||||
this.position = position
|
||||
this.stackItem = markRaw(toRaw(stackItem))
|
||||
}
|
||||
|
||||
static async create(
|
||||
static async Create(
|
||||
rpc: Promise<LanguageServer>,
|
||||
binary: Promise<DataServer>,
|
||||
contentRoots: Promise<ContentRoot[]>,
|
||||
awareness: Awareness,
|
||||
file: File,
|
||||
position: Vec2,
|
||||
stackItem: StackItem,
|
||||
): Promise<Uploader> {
|
||||
const projectRootId = await contentRoots.then((roots) =>
|
||||
roots.find((root) => root.type == 'Project'),
|
||||
)
|
||||
if (!projectRootId) throw new Error('Unable to find project root, uploading not possible.')
|
||||
const instance = new Uploader(await rpc, await binary, file, projectRootId.id)
|
||||
const instance = new Uploader(
|
||||
await rpc,
|
||||
await binary,
|
||||
awareness,
|
||||
file,
|
||||
projectRootId.id,
|
||||
position,
|
||||
stackItem,
|
||||
)
|
||||
return instance
|
||||
}
|
||||
|
||||
async upload(): Promise<string> {
|
||||
await this.ensureDataDirExists()
|
||||
const name = await this.pickUniqueName(this.file.name)
|
||||
const file: UploadingFile = {
|
||||
sizePercentage: 0,
|
||||
position: this.position,
|
||||
stackItem: this.stackItem,
|
||||
}
|
||||
this.awareness.addOrUpdateUpload(name, file)
|
||||
const remotePath: Path = { rootId: this.projectRootId, segments: [DATA_DIR_NAME, name] }
|
||||
const uploader = this
|
||||
const cleanup = this.cleanup.bind(this, name)
|
||||
const writableStream = new WritableStream<Uint8Array>({
|
||||
async write(chunk: Uint8Array) {
|
||||
await uploader.binary.writeBytes(remotePath, uploader.uploadedBytes, false, chunk)
|
||||
uploader.checksum.update(chunk)
|
||||
uploader.uploadedBytes += BigInt(chunk.length)
|
||||
const bytes = Number(uploader.uploadedBytes)
|
||||
const sizePercentage = Math.round((bytes / uploader.file.size) * 100)
|
||||
const file: UploadingFile = {
|
||||
sizePercentage,
|
||||
position: uploader.position,
|
||||
stackItem: uploader.stackItem,
|
||||
}
|
||||
uploader.awareness.addOrUpdateUpload(name, file)
|
||||
},
|
||||
async close() {
|
||||
cleanup()
|
||||
// Disabled until https://github.com/enso-org/enso/issues/6691 is fixed.
|
||||
// uploader.assertChecksum(remotePath)
|
||||
},
|
||||
async abort(reason: string) {
|
||||
cleanup()
|
||||
await uploader.rpc.deleteFile(remotePath)
|
||||
throw new Error(`Uploading process aborted. ${reason}`)
|
||||
},
|
||||
@ -64,6 +113,10 @@ export class Uploader {
|
||||
return name
|
||||
}
|
||||
|
||||
private cleanup(name: string) {
|
||||
this.awareness.removeUpload(name)
|
||||
}
|
||||
|
||||
private async assertChecksum(path: Path) {
|
||||
const engineChecksum = await this.rpc.fileChecksum(path)
|
||||
const hexChecksum = bytesToHex(this.checksum.digest())
|
||||
|
81
app/gui2/src/stores/awareness.ts
Normal file
81
app/gui2/src/stores/awareness.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import type { StackItem } from 'shared/languageServerTypes'
|
||||
import { reactive } from 'vue'
|
||||
import { Awareness as YjsAwareness } from 'y-protocols/awareness'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
// === Public types ===
|
||||
|
||||
export type FileName = string
|
||||
|
||||
export interface UploadingFile {
|
||||
sizePercentage: number
|
||||
stackItem: StackItem
|
||||
position: Vec2
|
||||
}
|
||||
|
||||
// === Awareness wrapper ===
|
||||
|
||||
/**
|
||||
* A thin wrapper around `Awareness` from `yjs`, providing helper methods for Enso IDE-specific state.
|
||||
*/
|
||||
export class Awareness {
|
||||
public internal: YjsAwareness
|
||||
private uploadingFiles: Map<ClientId, Uploads>
|
||||
|
||||
constructor(doc: Y.Doc) {
|
||||
this.internal = new YjsAwareness(doc)
|
||||
this.internal.setLocalState(initialState())
|
||||
this.uploadingFiles = reactive(new Map())
|
||||
|
||||
this.internal.on('update', (updates: AwarenessUpdates) => {
|
||||
updates.removed.forEach((id) => this.uploadingFiles.delete(id))
|
||||
for (const id of [...updates.added, ...updates.updated]) {
|
||||
const uploads = this.internal.getStates().get(id)?.uploads
|
||||
if (uploads) {
|
||||
this.uploadingFiles.set(id, structuredClone(uploads))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public addOrUpdateUpload(name: FileName, file: UploadingFile) {
|
||||
this.withUploads((uploads) => {
|
||||
uploads[name] = file
|
||||
})
|
||||
}
|
||||
|
||||
public removeUpload(name: FileName) {
|
||||
this.withUploads((uploads) => {
|
||||
delete uploads[name]
|
||||
})
|
||||
}
|
||||
|
||||
public allUploads(): Iterable<[FileName, UploadingFile]> {
|
||||
return [...this.uploadingFiles.values()].flatMap((uploads) => [...Object.entries(uploads)])
|
||||
}
|
||||
|
||||
private withUploads(f: (uploads: Uploads) => void) {
|
||||
const state = this.internal.getLocalState() as State
|
||||
f(state.uploads)
|
||||
this.internal.setLocalState(state)
|
||||
}
|
||||
}
|
||||
|
||||
// === Private types ===
|
||||
|
||||
type ClientId = number
|
||||
|
||||
interface State {
|
||||
uploads: Uploads
|
||||
}
|
||||
|
||||
type Uploads = Record<FileName, UploadingFile>
|
||||
|
||||
const initialState: () => State = () => ({ uploads: {} })
|
||||
|
||||
interface AwarenessUpdates {
|
||||
added: ClientId[]
|
||||
removed: ClientId[]
|
||||
updated: ClientId[]
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { injectGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
||||
import { Awareness } from '@/stores/awareness'
|
||||
import { bail } from '@/util/assert'
|
||||
import { ComputedValueRegistry } from '@/util/computedValueRegistry'
|
||||
import { attachProvider, useObserveYjs } from '@/util/crdt'
|
||||
@ -44,7 +45,6 @@ import {
|
||||
type WatchSource,
|
||||
type WritableComputedRef,
|
||||
} from 'vue'
|
||||
import { Awareness } from 'y-protocols/awareness'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
interface LsUrls {
|
||||
@ -472,7 +472,13 @@ export const useProjectStore = defineStore('project', () => {
|
||||
const socketUrl = new URL(location.origin)
|
||||
socketUrl.protocol = location.protocol.replace(/^http/, 'ws')
|
||||
socketUrl.pathname = '/project'
|
||||
const provider = attachProvider(socketUrl.href, 'index', { ls: lsUrls.rpcUrl }, doc, awareness)
|
||||
const provider = attachProvider(
|
||||
socketUrl.href,
|
||||
'index',
|
||||
{ ls: lsUrls.rpcUrl },
|
||||
doc,
|
||||
awareness.internal,
|
||||
)
|
||||
onCleanup(() => {
|
||||
provider.dispose()
|
||||
})
|
||||
@ -559,7 +565,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
modulePath,
|
||||
projectModel,
|
||||
contentRoots,
|
||||
awareness,
|
||||
awareness: markRaw(awareness),
|
||||
computedValueRegistry,
|
||||
lsRpcConnection: markRaw(lsRpcConnection),
|
||||
dataConnection: markRaw(dataConnection),
|
||||
|
@ -158,7 +158,9 @@ export function onOpenUrl(url: URL, window: () => electron.BrowserWindow) {
|
||||
* The credentials file is placed in the user's home directory in the `.enso` subdirectory
|
||||
* in the `credentials` file. */
|
||||
function initSaveAccessTokenListener() {
|
||||
electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string) => {
|
||||
electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string | null) => {
|
||||
event.preventDefault()
|
||||
|
||||
/** Home directory for the credentials file. */
|
||||
const credentialsDirectoryName = `.${common.PRODUCT_NAME.toLowerCase()}`
|
||||
/** File name of the credentials file. */
|
||||
@ -166,22 +168,28 @@ function initSaveAccessTokenListener() {
|
||||
/** System agnostic credentials directory home path. */
|
||||
const credentialsHomePath = path.join(os.homedir(), credentialsDirectoryName)
|
||||
|
||||
fs.mkdir(credentialsHomePath, { recursive: true }, error => {
|
||||
if (error) {
|
||||
logger.error(`Couldn't create ${credentialsDirectoryName} directory.`)
|
||||
} else {
|
||||
fs.writeFile(
|
||||
path.join(credentialsHomePath, credentialsFileName),
|
||||
accessToken,
|
||||
innerError => {
|
||||
if (innerError) {
|
||||
logger.error(`Could not write to ${credentialsFileName} file.`)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (accessToken == null) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(credentialsHomePath, credentialsFileName))
|
||||
} catch {
|
||||
// Ignored, most likely the path does not exist.
|
||||
}
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
} else {
|
||||
fs.mkdir(credentialsHomePath, { recursive: true }, error => {
|
||||
if (error) {
|
||||
logger.error(`Couldn't create ${credentialsDirectoryName} directory.`)
|
||||
} else {
|
||||
fs.writeFile(
|
||||
path.join(credentialsHomePath, credentialsFileName),
|
||||
accessToken,
|
||||
innerError => {
|
||||
if (innerError) {
|
||||
logger.error(`Could not write to ${credentialsFileName} file.`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ const AUTHENTICATION_API = {
|
||||
*
|
||||
* The backend doesn't have access to Electron's `localStorage` so we need to save access token
|
||||
* to a file. Then the token will be used to sign cloud API requests. */
|
||||
saveAccessToken: (accessToken: string) => {
|
||||
saveAccessToken: (accessToken: string | null) => {
|
||||
electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessToken)
|
||||
},
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./src/detect": "./src/detect.ts"
|
||||
"./src/detect": "./src/detect.ts",
|
||||
"./src/gtag": "./src/gtag.ts"
|
||||
}
|
||||
}
|
||||
|
26
app/ide-desktop/lib/common/src/gtag.ts
Normal file
26
app/ide-desktop/lib/common/src/gtag.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/** @file Google Analytics tag. */
|
||||
|
||||
// @ts-expect-error This is explicitly not given types as it is a mistake to acess this
|
||||
// anywhere else.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/strict-boolean-expressions
|
||||
window.dataLayer = window.dataLayer || []
|
||||
|
||||
/** Google Analytics tag function. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function gtag(_action: 'config' | 'event' | 'js' | 'set', ..._args: unknown[]) {
|
||||
// @ts-expect-error This is explicitly not given types as it is a mistake to acess this
|
||||
// anywhere else.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
window.dataLayer.push(arguments)
|
||||
}
|
||||
|
||||
/** Send event to Google Analytics. */
|
||||
export function event(name: string, params?: object) {
|
||||
gtag('event', name, params)
|
||||
}
|
||||
|
||||
gtag('js', new Date())
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
gtag('set', 'linker', { accept_incoming: true })
|
||||
gtag('config', 'G-CLTBJ37MDM')
|
||||
gtag('config', 'G-DH47F649JC')
|
@ -26,9 +26,12 @@ self.addEventListener('install', event => {
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url)
|
||||
if (url.hostname === 'localhost' && url.pathname === '/esbuild') {
|
||||
if (
|
||||
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
|
||||
url.pathname === '/esbuild'
|
||||
) {
|
||||
return false
|
||||
} else if (url.hostname === 'localhost') {
|
||||
} else if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
||||
const responsePromise = caches
|
||||
.open(constants.CACHE_NAME)
|
||||
.then(cache => cache.match(event.request))
|
||||
|
@ -47,5 +47,15 @@
|
||||
<noscript>
|
||||
This page requires JavaScript to run. Please enable it in your browser.
|
||||
</noscript>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.2/dist/twemoji.min.js"
|
||||
integrity="sha384-D6GSzpW7fMH86ilu73eB95ipkfeXcMPoOGVst/L04yqSSe+RTUY0jXcuEIZk0wrT"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,7 @@ import * as common from 'enso-common'
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
import * as dashboard from 'enso-authentication'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
import * as gtag from 'enso-common/src/gtag'
|
||||
|
||||
import * as remoteLog from './remoteLog'
|
||||
import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' }
|
||||
@ -251,6 +252,7 @@ class Main implements AppRunner {
|
||||
const isInAuthenticationFlow = url.searchParams.has('code') && url.searchParams.has('state')
|
||||
const authenticationUrl = location.href
|
||||
if (isInAuthenticationFlow) {
|
||||
gtag.gtag('event', 'cloud_sign_in_redirect')
|
||||
history.replaceState(null, '', localStorage.getItem(INITIAL_URL_KEY))
|
||||
}
|
||||
const configOptions = contentConfig.OPTIONS.clone()
|
||||
|
@ -24,7 +24,7 @@ self.addEventListener('install', event => {
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url)
|
||||
if (url.hostname === 'localhost') {
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
||||
return false
|
||||
} else {
|
||||
event.respondWith(
|
||||
|
@ -181,8 +181,8 @@ export class Cognito {
|
||||
}
|
||||
|
||||
/** Save the access token to a file for further reuse. */
|
||||
saveAccessToken(accessToken: string) {
|
||||
this.amplifyConfig.accessTokenSaver?.(accessToken)
|
||||
saveAccessToken(accessToken: string | null) {
|
||||
this.amplifyConfig.saveAccessToken?.(accessToken)
|
||||
}
|
||||
|
||||
/** Return the current {@link UserSession}, or `None` if the user is not logged in.
|
||||
|
@ -95,7 +95,7 @@ export default function ControlledInput(props: ControlledInputProps) {
|
||||
}
|
||||
: onBlur
|
||||
}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full border w-full py-2"
|
||||
className="text-sm placeholder-gray-500 hover:bg-gray-100 focus:bg-gray-100 pl-10 pr-4 rounded-full border transition-all duration-300 w-full py-2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export default function Link(props: LinkProps) {
|
||||
return (
|
||||
<router.Link
|
||||
to={to}
|
||||
className="flex gap-2 items-center font-bold text-blue-500 hover:text-blue-700 text-xs text-center"
|
||||
className="flex gap-2 items-center font-bold text-blue-500 hover:text-blue-700 focus:text-blue-700 text-xs text-center transition-all duration-300"
|
||||
>
|
||||
<SvgMask src={icon} />
|
||||
{text}
|
||||
|
@ -55,7 +55,7 @@ export default function Login() {
|
||||
event.preventDefault()
|
||||
await signInWithGoogle()
|
||||
}}
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-cloud/20 focus:bg-cloud/20 transition-all duration-300 py-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGoogle} />
|
||||
Sign up or login with Google
|
||||
@ -68,7 +68,7 @@ export default function Login() {
|
||||
event.preventDefault()
|
||||
await signInWithGitHub()
|
||||
}}
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-cloud/20 focus:bg-cloud/20 transition-all duration-300 py-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
|
||||
Sign up or login with GitHub
|
||||
@ -117,7 +117,7 @@ export default function Login() {
|
||||
footer={
|
||||
<router.Link
|
||||
to={app.FORGOT_PASSWORD_PATH}
|
||||
className="text-xs text-blue-500 hover:text-blue-700 text-end"
|
||||
className="text-xs text-blue-500 hover:text-blue-700 focus:text-blue-700 transition-all duration-300 text-end"
|
||||
>
|
||||
Forgot Your Password?
|
||||
</router.Link>
|
||||
|
@ -8,6 +8,8 @@ import GoBackIcon from 'enso-assets/go_back.svg'
|
||||
import LockIcon from 'enso-assets/lock.svg'
|
||||
|
||||
import * as authModule from '../providers/auth'
|
||||
import * as localStorageModule from '../../dashboard/localStorage'
|
||||
import * as localStorageProvider from '../../providers/localStorage'
|
||||
import * as string from '../../string'
|
||||
import * as validation from '../../dashboard/validation'
|
||||
|
||||
@ -22,6 +24,7 @@ import SubmitButton from './submitButton'
|
||||
|
||||
const REGISTRATION_QUERY_PARAMS = {
|
||||
organizationId: 'organization_id',
|
||||
redirectTo: 'redirect_to',
|
||||
} as const
|
||||
|
||||
// ====================
|
||||
@ -32,11 +35,20 @@ const REGISTRATION_QUERY_PARAMS = {
|
||||
export default function Registration() {
|
||||
const auth = authModule.useAuth()
|
||||
const location = router.useLocation()
|
||||
const { localStorage } = localStorageProvider.useLocalStorage()
|
||||
const [email, setEmail] = React.useState('')
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [confirmPassword, setConfirmPassword] = React.useState('')
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
const { organizationId } = parseUrlSearchParams(location.search)
|
||||
const { organizationId, redirectTo } = parseUrlSearchParams(location.search)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (redirectTo != null) {
|
||||
localStorage.set(localStorageModule.LocalStorageKey.loginRedirect, redirectTo)
|
||||
} else {
|
||||
localStorage.delete(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
}
|
||||
}, [localStorage, redirectTo])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
@ -98,5 +110,6 @@ export default function Registration() {
|
||||
function parseUrlSearchParams(search: string) {
|
||||
const query = new URLSearchParams(search)
|
||||
const organizationId = query.get(REGISTRATION_QUERY_PARAMS.organizationId)
|
||||
return { organizationId }
|
||||
const redirectTo = query.get(REGISTRATION_QUERY_PARAMS.redirectTo)
|
||||
return { organizationId, redirectTo }
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export default function SubmitButton(props: SubmitButtonProps) {
|
||||
<button
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
className="flex gap-2 items-center justify-center focus:outline-none text-white bg-blue-600 hover:bg-blue-700 rounded-full py-2 w-full transition duration-150 ease-in disabled:opacity-50"
|
||||
className="flex gap-2 items-center justify-center focus:outline-none text-white bg-blue-600 hover:bg-blue-700 focus:bg-blue-700 rounded-full py-2 w-full transition-all duration-300 ease-in disabled:opacity-50"
|
||||
>
|
||||
{text}
|
||||
<SvgMask src={icon} />
|
||||
|
@ -71,7 +71,7 @@ export const OAuthRedirect = newtype.newtypeConstructor<OAuthRedirect>()
|
||||
export type OAuthUrlOpener = (url: string, redirectUrl: string) => void
|
||||
/** A function used to save the access token to a credentials file. The token is used by the engine
|
||||
* to issue HTTP requests to the cloud API. */
|
||||
export type AccessTokenSaver = (accessToken: string) => void
|
||||
export type AccessTokenSaver = (accessToken: string | null) => void
|
||||
/** Function used to register a callback. The callback will get called when a deep link is received
|
||||
* by the app. This is only used in the desktop app (i.e., not in the cloud). This is used when the
|
||||
* user is redirected back to the app from the system browser, after completing an OAuth flow. */
|
||||
@ -96,7 +96,6 @@ export const OAUTH_RESPONSE_TYPE = OAuthResponseType('code')
|
||||
// === AmplifyConfig ===
|
||||
// =====================
|
||||
|
||||
// Eslint does not like "etc.".
|
||||
/** Configuration for the AWS Amplify library.
|
||||
*
|
||||
* This details user pools, federated identity providers, etc. that are used to authenticate users.
|
||||
@ -107,7 +106,7 @@ export interface AmplifyConfig {
|
||||
userPoolId: UserPoolId
|
||||
userPoolWebClientId: UserPoolWebClientId
|
||||
urlOpener: OAuthUrlOpener | null
|
||||
accessTokenSaver: AccessTokenSaver | null
|
||||
saveAccessToken: AccessTokenSaver | null
|
||||
domain: OAuthDomain
|
||||
scope: OAuthScope[]
|
||||
redirectSignIn: OAuthRedirect
|
||||
|
@ -9,6 +9,8 @@ import * as toast from 'react-toastify'
|
||||
|
||||
import * as sentry from '@sentry/react'
|
||||
|
||||
import * as gtag from 'enso-common/src/gtag'
|
||||
|
||||
import * as app from '../../components/app'
|
||||
import type * as authServiceModule from '../service'
|
||||
import * as backendModule from '../../dashboard/backend'
|
||||
@ -17,6 +19,7 @@ import * as cognitoModule from '../cognito'
|
||||
import * as errorModule from '../../error'
|
||||
import * as http from '../../http'
|
||||
import * as localBackend from '../../dashboard/localBackend'
|
||||
import * as localStorageModule from '../../dashboard/localStorage'
|
||||
import * as localStorageProvider from '../../providers/localStorage'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as remoteBackend from '../../dashboard/remoteBackend'
|
||||
@ -272,6 +275,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
) {
|
||||
setBackendWithoutSavingType(backend)
|
||||
}
|
||||
gtag.event('cloud_open')
|
||||
let organization: backendModule.UserOrOrganization | null
|
||||
let user: backendModule.SimpleUser | null
|
||||
while (true) {
|
||||
@ -279,7 +283,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
organization = await backend.usersMe()
|
||||
try {
|
||||
user =
|
||||
organization != null
|
||||
organization?.isEnabled === true
|
||||
? (await backend.listUsers()).find(
|
||||
listedUser => listedUser.email === organization?.email
|
||||
) ?? null
|
||||
@ -331,11 +335,15 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
user,
|
||||
}
|
||||
|
||||
/** Save access token so can be reused by Enso backend. */
|
||||
// 34560000 is the recommended max cookie age.
|
||||
const parentDomain = location.hostname.replace(/^[^.]*\./, '')
|
||||
document.cookie = `logged_in=yes;max-age=34560000;domain=${parentDomain};samesite=strict;secure`
|
||||
|
||||
// Save access token so can it be reused by the backend.
|
||||
cognito.saveAccessToken(session.accessToken)
|
||||
|
||||
/** Execute the callback that should inform the Electron app that the user has logged in.
|
||||
* This is done to transition the app from the authentication/dashboard view to the IDE. */
|
||||
// Execute the callback that should inform the Electron app that the user has logged in.
|
||||
// This is done to transition the app from the authentication/dashboard view to the IDE.
|
||||
onAuthenticated(session.accessToken)
|
||||
}
|
||||
|
||||
@ -400,6 +408,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
}
|
||||
|
||||
const signUp = async (username: string, password: string, organizationId: string | null) => {
|
||||
gtag.event('cloud_sign_up')
|
||||
const result = await cognito.signUp(username, password, organizationId)
|
||||
if (result.ok) {
|
||||
toastSuccess(MESSAGES.signUpSuccess)
|
||||
@ -411,6 +420,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
}
|
||||
|
||||
const confirmSignUp = async (email: string, code: string) => {
|
||||
gtag.event('cloud_confirm_sign_up')
|
||||
const result = await cognito.confirmSignUp(email, code)
|
||||
if (result.err) {
|
||||
switch (result.val.kind) {
|
||||
@ -426,6 +436,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
}
|
||||
|
||||
const signInWithPassword = async (email: string, password: string) => {
|
||||
gtag.event('cloud_sign_in', { provider: 'Email' })
|
||||
const result = await cognito.signInWithPassword(email, password)
|
||||
if (result.ok) {
|
||||
toastSuccess(MESSAGES.signInWithPasswordSuccess)
|
||||
@ -443,6 +454,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
toastError('You cannot set your username on the local backend.')
|
||||
return false
|
||||
} else {
|
||||
gtag.event('cloud_user_created')
|
||||
try {
|
||||
const organizationId = await authService.cognito.organizationId()
|
||||
// This should not omit success and error toasts as it is not possible
|
||||
@ -462,7 +474,15 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
pending: MESSAGES.setUsernameLoading,
|
||||
}
|
||||
)
|
||||
navigate(app.DASHBOARD_PATH)
|
||||
const redirectTo = localStorage.get(
|
||||
localStorageModule.LocalStorageKey.loginRedirect
|
||||
)
|
||||
if (redirectTo != null) {
|
||||
localStorage.delete(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
location.href = redirectTo
|
||||
} else {
|
||||
navigate(app.DASHBOARD_PATH)
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
@ -503,11 +523,15 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
const parentDomain = location.hostname.replace(/^[^.]*\./, '')
|
||||
document.cookie = `logged_in=no;max-age=0;domain=${parentDomain}`
|
||||
gtag.event('cloud_sign_out')
|
||||
cognito.saveAccessToken(null)
|
||||
localStorage.clearUserSpecificEntries()
|
||||
deinitializeSession()
|
||||
setInitialized(false)
|
||||
sentry.setUser(null)
|
||||
setUserSession(null)
|
||||
localStorage.clearUserSpecificEntries()
|
||||
// This should not omit success and error toasts as it is not possible
|
||||
// to render this optimistically.
|
||||
await toast.toast.promise(cognito.signOut(), {
|
||||
@ -523,16 +547,20 @@ export function AuthProvider(props: AuthProviderProps) {
|
||||
signUp: withLoadingToast(signUp),
|
||||
confirmSignUp: withLoadingToast(confirmSignUp),
|
||||
setUsername,
|
||||
signInWithGoogle: () =>
|
||||
cognito.signInWithGoogle().then(
|
||||
signInWithGoogle: () => {
|
||||
gtag.event('cloud_sign_in', { provider: 'Google' })
|
||||
return cognito.signInWithGoogle().then(
|
||||
() => true,
|
||||
() => false
|
||||
),
|
||||
signInWithGitHub: () =>
|
||||
cognito.signInWithGitHub().then(
|
||||
)
|
||||
},
|
||||
signInWithGitHub: () => {
|
||||
gtag.event('cloud_sign_in', { provider: 'GitHub' })
|
||||
return cognito.signInWithGitHub().then(
|
||||
() => true,
|
||||
() => false
|
||||
),
|
||||
)
|
||||
},
|
||||
signInWithPassword: withLoadingToast(signInWithPassword),
|
||||
forgotPassword: withLoadingToast(forgotPassword),
|
||||
resetPassword: withLoadingToast(resetPassword),
|
||||
@ -611,10 +639,18 @@ export function ProtectedLayout() {
|
||||
* in the process of registering. */
|
||||
export function SemiProtectedLayout() {
|
||||
const { session } = useAuth()
|
||||
const { localStorage } = localStorageProvider.useLocalStorage()
|
||||
const shouldPreventNavigation = getShouldPreventNavigation()
|
||||
|
||||
if (!shouldPreventNavigation && session?.type === UserSessionType.full) {
|
||||
return <router.Navigate to={app.DASHBOARD_PATH} />
|
||||
const redirectTo = localStorage.get(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
if (redirectTo != null) {
|
||||
localStorage.delete(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
location.href = redirectTo
|
||||
return
|
||||
} else {
|
||||
return <router.Navigate to={app.DASHBOARD_PATH} />
|
||||
}
|
||||
} else {
|
||||
return <router.Outlet context={session} />
|
||||
}
|
||||
@ -628,12 +664,20 @@ export function SemiProtectedLayout() {
|
||||
* not logged in. */
|
||||
export function GuestLayout() {
|
||||
const { session } = useAuth()
|
||||
const { localStorage } = localStorageProvider.useLocalStorage()
|
||||
const shouldPreventNavigation = getShouldPreventNavigation()
|
||||
|
||||
if (!shouldPreventNavigation && session?.type === UserSessionType.partial) {
|
||||
return <router.Navigate to={app.SET_USERNAME_PATH} />
|
||||
} else if (!shouldPreventNavigation && session?.type === UserSessionType.full) {
|
||||
return <router.Navigate to={app.DASHBOARD_PATH} />
|
||||
const redirectTo = localStorage.get(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
if (redirectTo != null) {
|
||||
localStorage.delete(localStorageModule.LocalStorageKey.loginRedirect)
|
||||
location.href = redirectTo
|
||||
return
|
||||
} else {
|
||||
return <router.Navigate to={app.DASHBOARD_PATH} />
|
||||
}
|
||||
} else {
|
||||
return <router.Outlet />
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ function loadAmplifyConfig(
|
||||
/** Load the environment-specific Amplify configuration. */
|
||||
const baseConfig = AMPLIFY_CONFIGS[config.ENVIRONMENT]
|
||||
let urlOpener: ((url: string) => void) | null = null
|
||||
let accessTokenSaver: ((accessToken: string) => void) | null = null
|
||||
let accessTokenSaver: ((accessToken: string | null) => void) | null = null
|
||||
if ('authenticationApi' in window) {
|
||||
/** When running on destop we want to have option to save access token to a file,
|
||||
* so it can be later reuse when issuing requests to Cloud API. */
|
||||
@ -164,7 +164,7 @@ function loadAmplifyConfig(
|
||||
...baseConfig,
|
||||
...platformConfig,
|
||||
urlOpener,
|
||||
accessTokenSaver,
|
||||
saveAccessToken: accessTokenSaver,
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ function openUrlWithExternalBrowser(url: string) {
|
||||
}
|
||||
|
||||
/** Save the access token to a file. */
|
||||
function saveAccessToken(accessToken: string) {
|
||||
function saveAccessToken(accessToken: string | null) {
|
||||
window.authenticationApi.saveAccessToken(accessToken)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import DefaultUserIcon from 'enso-assets/default_user.svg'
|
||||
import TriangleDownIcon from 'enso-assets/triangle_down.svg'
|
||||
|
||||
import * as chat from 'enso-chat/chat'
|
||||
import * as gtag from 'enso-common/src/gtag'
|
||||
|
||||
import * as animations from '../../animations'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
@ -263,8 +264,10 @@ function ChatHeader(props: InternalChatHeaderProps) {
|
||||
setIsThreadListVisible(false)
|
||||
}
|
||||
document.addEventListener('click', onClick)
|
||||
gtag.event('cloud_open_chat')
|
||||
return () => {
|
||||
document.removeEventListener('click', onClick)
|
||||
gtag.event('cloud_close_chat')
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -116,11 +116,14 @@ export default function Drive(props: DriveProps) {
|
||||
|
||||
React.useEffect(() => {
|
||||
void (async () => {
|
||||
if (backend.type !== backendModule.BackendType.local) {
|
||||
if (
|
||||
backend.type !== backendModule.BackendType.local &&
|
||||
organization?.isEnabled === true
|
||||
) {
|
||||
setLabels(await backend.listTags())
|
||||
}
|
||||
})()
|
||||
}, [backend])
|
||||
}, [backend, organization?.isEnabled])
|
||||
|
||||
const doUploadFiles = React.useCallback(
|
||||
(files: File[]) => {
|
||||
|
@ -20,6 +20,7 @@ export enum LocalStorageKey {
|
||||
isTemplatesListOpen = 'is-templates-list-open',
|
||||
projectStartupInfo = 'project-startup-info',
|
||||
driveCategory = 'drive-category',
|
||||
loginRedirect = 'login-redirect',
|
||||
}
|
||||
|
||||
/** The data that can be stored in a {@link LocalStorage}. */
|
||||
@ -30,6 +31,7 @@ interface LocalStorageData {
|
||||
[LocalStorageKey.isTemplatesListOpen]: boolean
|
||||
[LocalStorageKey.projectStartupInfo]: backend.ProjectStartupInfo
|
||||
[LocalStorageKey.driveCategory]: categorySwitcher.Category
|
||||
[LocalStorageKey.loginRedirect]: string
|
||||
}
|
||||
|
||||
/** Whether each {@link LocalStorageKey} is user specific.
|
||||
@ -42,6 +44,7 @@ const IS_USER_SPECIFIC: Record<LocalStorageKey, boolean> = {
|
||||
[LocalStorageKey.isTemplatesListOpen]: false,
|
||||
[LocalStorageKey.projectStartupInfo]: true,
|
||||
[LocalStorageKey.driveCategory]: false,
|
||||
[LocalStorageKey.loginRedirect]: true,
|
||||
}
|
||||
|
||||
/** A LocalStorage data manager. */
|
||||
@ -120,6 +123,12 @@ export class LocalStorage {
|
||||
savedValues[LocalStorageKey.driveCategory]
|
||||
}
|
||||
}
|
||||
if (LocalStorageKey.loginRedirect in savedValues) {
|
||||
const value = savedValues[LocalStorageKey.loginRedirect]
|
||||
if (typeof value === 'string') {
|
||||
this.values[LocalStorageKey.loginRedirect] = value
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.values[LocalStorageKey.projectStartupInfo] == null &&
|
||||
this.values[LocalStorageKey.page] === pageSwitcher.Page.editor
|
||||
|
@ -19,7 +19,10 @@ declare const self: ServiceWorkerGlobalScope
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url)
|
||||
if (url.hostname === 'localhost' && url.pathname !== '/esbuild') {
|
||||
if (
|
||||
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
|
||||
url.pathname !== '/esbuild'
|
||||
) {
|
||||
const responsePromise = /\/[^.]+$/.test(new URL(event.request.url).pathname)
|
||||
? fetch('/index.html')
|
||||
: fetch(event.request.url)
|
||||
|
2
app/ide-desktop/lib/types/globals.d.ts
vendored
2
app/ide-desktop/lib/types/globals.d.ts
vendored
@ -50,7 +50,7 @@ interface AuthenticationApi {
|
||||
* via a deep link. See {@link setDeepLinkHandler} for details. */
|
||||
setDeepLinkHandler: (callback: (url: string) => void) => void
|
||||
/** Saves the access token to a file. */
|
||||
saveAccessToken: (access_token: string) => void
|
||||
saveAccessToken: (accessToken: string | null) => void
|
||||
}
|
||||
|
||||
// =====================================
|
||||
|
@ -3788,11 +3788,33 @@ interface ExecutionContextExecutionStatusNotification {
|
||||
|
||||
### `executionContext/executeExpression`
|
||||
|
||||
This message allows the client to execute an arbitrary expression on a given
|
||||
node. It behaves like oneshot
|
||||
This message allows the client to execute an arbitrary expression in a context
|
||||
of a given node. It behaves like putting a breakpoint after the expression with
|
||||
`expressionId` and executing the provided `expression`. All the local and global
|
||||
symbols that are available for the `expressionId` will be available when
|
||||
executing the `expression`. The result of the evaluation will be delivered as a
|
||||
visualization result on a binary connection. You can think of it as a oneshot
|
||||
[`executionContext/attachVisualization`](#executioncontextattachvisualization)
|
||||
visualization request, meaning that the visualization expression will be
|
||||
executed only once.
|
||||
visualization request, meaning that the expression will be executed once.
|
||||
|
||||
For example, given the current code:
|
||||
|
||||
```python
|
||||
main =
|
||||
operator1 = 42
|
||||
operator2 = operator1 + 1
|
||||
|
||||
fun1 x = x.to_text
|
||||
```
|
||||
|
||||
- You can execute an expression in the context of a function body. In this case,
|
||||
the `expressionId` should point to the body of a function. E.g. in the context
|
||||
of `main` available symbols are `operator1`, `operator2` and `fun1`.
|
||||
- Execute expression in the context of a local binding. E.g. in the context of
|
||||
`operator2 = operator1 + 1` available symbols are `operator1`, `operator2` and
|
||||
`fun1`.
|
||||
- Execute expression in the context of arbitrary expression. E.g. in the context
|
||||
of `operator1 + 1` available symbols are `operator1` and `fun1`.
|
||||
|
||||
- **Type:** Request
|
||||
- **Direction:** Client -> Server
|
||||
@ -3803,9 +3825,10 @@ executed only once.
|
||||
|
||||
```typescript
|
||||
interface ExecutionContextExecuteExpressionParameters {
|
||||
executionContextId: UUID;
|
||||
visualizationId: UUID;
|
||||
expressionId: UUID;
|
||||
visualizationConfig: VisualizationConfiguration;
|
||||
expression: string;
|
||||
}
|
||||
```
|
||||
|
||||
@ -3821,11 +3844,8 @@ type ExecutionContextExecuteExpressionResult = null;
|
||||
`executionContext/canModify` capability for this context.
|
||||
- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found
|
||||
by provided id.
|
||||
- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the module with
|
||||
the visualization cannot be found.
|
||||
- [`VisualizationExpressionError`](#visualizationexpressionerror) to signal that
|
||||
the expression specified in the `VisualizationConfiguration` cannot be
|
||||
evaluated.
|
||||
the provided expression cannot be evaluated.
|
||||
|
||||
### `executionContext/attachVisualization`
|
||||
|
||||
|
@ -37,9 +37,9 @@ logging-service {
|
||||
org.eclipse.jgit = error
|
||||
io.methvin.watcher = error
|
||||
# Log levels to limit during very verbose setting:
|
||||
#org.enso.languageserver.protocol.json.JsonConnectionController = debug
|
||||
#org.enso.jsonrpc.JsonRpcServer = debug
|
||||
#org.enso.languageserver.runtime.RuntimeConnector = debug
|
||||
org.enso.languageserver.protocol.json.JsonConnectionController = debug
|
||||
org.enso.jsonrpc.JsonRpcServer = debug
|
||||
org.enso.languageserver.runtime.RuntimeConnector = debug
|
||||
}
|
||||
appenders = [
|
||||
{
|
||||
@ -56,7 +56,7 @@ logging-service {
|
||||
default-appender = socket
|
||||
default-appender = ${?ENSO_APPENDER_DEFAULT}
|
||||
log-to-file {
|
||||
enable = false
|
||||
enable = false ## Will have effect only if language server is not using socket appender
|
||||
enable = ${?ENSO_LOG_TO_FILE}
|
||||
log-level = debug
|
||||
log-level = ${?ENSO_LOG_TO_FILE_LOG_LEVEL}
|
||||
|
@ -40,9 +40,10 @@ class ExecuteExpressionHandler(
|
||||
) =>
|
||||
contextRegistry ! ContextRegistryProtocol.ExecuteExpression(
|
||||
clientId,
|
||||
params.executionContextId,
|
||||
params.visualizationId,
|
||||
params.expressionId,
|
||||
params.visualizationConfig
|
||||
params.expression
|
||||
)
|
||||
val cancellable =
|
||||
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||
|
@ -261,8 +261,13 @@ final class ContextRegistry(
|
||||
sender() ! AccessDenied
|
||||
}
|
||||
|
||||
case ExecuteExpression(clientId, visualizationId, expressionId, cfg) =>
|
||||
val contextId = cfg.executionContextId
|
||||
case ExecuteExpression(
|
||||
clientId,
|
||||
contextId,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
expression
|
||||
) =>
|
||||
if (store.hasContext(clientId, contextId)) {
|
||||
store.getListener(contextId).foreach { listener =>
|
||||
listener ! RegisterOneshotVisualization(
|
||||
@ -272,17 +277,18 @@ final class ContextRegistry(
|
||||
)
|
||||
}
|
||||
val handler = context.actorOf(
|
||||
AttachVisualizationHandler.props(
|
||||
ExecuteExpressionHandler.props(
|
||||
runtimeFailureMapper,
|
||||
timeout,
|
||||
runtime
|
||||
)
|
||||
)
|
||||
handler.forward(
|
||||
Api.AttachVisualization(
|
||||
Api.ExecuteExpression(
|
||||
contextId,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
cfg.toApi
|
||||
expression
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
@ -9,7 +9,7 @@ import org.enso.languageserver.filemanager.{FileSystemFailure, Path}
|
||||
import org.enso.languageserver.libraries.LibraryComponentGroup
|
||||
import org.enso.languageserver.runtime.ExecutionApi.ContextId
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.logger.masking.ToLogString
|
||||
import org.enso.logger.masking.{MaskedString, ToLogString}
|
||||
import org.enso.text.editing.model
|
||||
|
||||
import java.util.UUID
|
||||
@ -422,14 +422,14 @@ object ContextRegistryProtocol {
|
||||
* @param clientId the requester id
|
||||
* @param visualizationId an identifier of a visualization
|
||||
* @param expressionId an identifier of an expression which is visualised
|
||||
* @param visualizationConfig a configuration object for properties of the
|
||||
* visualization
|
||||
* @param expression the expression to execute
|
||||
*/
|
||||
case class ExecuteExpression(
|
||||
clientId: ClientId,
|
||||
executionContextId: UUID,
|
||||
visualizationId: UUID,
|
||||
expressionId: UUID,
|
||||
visualizationConfig: VisualizationConfiguration
|
||||
expression: String
|
||||
) extends ToLogString {
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -437,8 +437,8 @@ object ContextRegistryProtocol {
|
||||
"ExecuteExpression(" +
|
||||
s"clientId=$clientId," +
|
||||
s"visualizationId=$visualizationId," +
|
||||
s"expressionId=$expressionId,visualizationConfig=" +
|
||||
visualizationConfig.toLogString(shouldMask) +
|
||||
s"expressionId=$expressionId,expression=" +
|
||||
MaskedString(expression).toLogString(shouldMask) +
|
||||
")"
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,10 @@ object VisualizationApi {
|
||||
extends Method("executionContext/executeExpression") {
|
||||
|
||||
case class Params(
|
||||
executionContextId: UUID,
|
||||
visualizationId: UUID,
|
||||
expressionId: UUID,
|
||||
visualizationConfig: VisualizationConfiguration
|
||||
expression: String
|
||||
)
|
||||
|
||||
implicit val hasParams: HasParams.Aux[this.type, ExecuteExpression.Params] =
|
||||
|
@ -0,0 +1,81 @@
|
||||
package org.enso.languageserver.runtime.handler
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||
import akka.pattern.pipe
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import org.enso.languageserver.requesthandler.RequestTimeout
|
||||
import org.enso.languageserver.runtime.{
|
||||
ContextRegistryProtocol,
|
||||
RuntimeFailureMapper
|
||||
}
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** A request handler for execute expression commands.
|
||||
*
|
||||
* @param runtimeFailureMapper mapper for runtime failures
|
||||
* @param timeout request timeout
|
||||
* @param runtime reference to the runtime connector
|
||||
*/
|
||||
class ExecuteExpressionHandler(
|
||||
runtimeFailureMapper: RuntimeFailureMapper,
|
||||
timeout: FiniteDuration,
|
||||
runtime: ActorRef
|
||||
) extends Actor
|
||||
with LazyLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def receive: Receive = requestStage
|
||||
|
||||
private def requestStage: Receive = { case msg: Api.ExecuteExpression =>
|
||||
runtime ! Api.Request(UUID.randomUUID(), msg)
|
||||
val cancellable =
|
||||
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||
context.become(responseStage(sender(), cancellable))
|
||||
}
|
||||
|
||||
private def responseStage(
|
||||
replyTo: ActorRef,
|
||||
cancellable: Cancellable
|
||||
): Receive = {
|
||||
case RequestTimeout =>
|
||||
replyTo ! RequestTimeout
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, Api.VisualizationAttached()) =>
|
||||
replyTo ! ContextRegistryProtocol.VisualizationAttached
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, error: Api.Error) =>
|
||||
runtimeFailureMapper.mapApiError(error).pipeTo(replyTo)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ExecuteExpressionHandler {
|
||||
|
||||
/** Creates configuration object used to create a [[ExecuteExpressionHandler]].
|
||||
*
|
||||
* @param runtimeFailureMapper mapper for runtime failures
|
||||
* @param timeout request timeout
|
||||
* @param runtime reference to the runtime connector
|
||||
*/
|
||||
def props(
|
||||
runtimeFailureMapper: RuntimeFailureMapper,
|
||||
timeout: FiniteDuration,
|
||||
runtime: ActorRef
|
||||
): Props =
|
||||
Props(
|
||||
new ExecuteExpressionHandler(runtimeFailureMapper, timeout, runtime)
|
||||
)
|
||||
|
||||
}
|
@ -631,21 +631,21 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
// attach visualization
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val expressionId = UUID.randomUUID()
|
||||
val config =
|
||||
VisualizationConfiguration(contextId, "Test.Main", ".to_json.to_text")
|
||||
client.send(
|
||||
json.executionContextExecuteExpressionRequest(
|
||||
2,
|
||||
contextId,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
config
|
||||
"expression"
|
||||
)
|
||||
)
|
||||
val requestId2 =
|
||||
runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(
|
||||
requestId,
|
||||
Api.AttachVisualization(
|
||||
Api.ExecuteExpression(
|
||||
`contextId`,
|
||||
`visualizationId`,
|
||||
`expressionId`,
|
||||
_
|
||||
@ -662,63 +662,6 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
client.expectJson(json.ok(2))
|
||||
}
|
||||
|
||||
"return ModuleNotFound error when executing expression" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
// create context
|
||||
client.send(json.executionContextCreateRequest(1))
|
||||
val (requestId, contextId) =
|
||||
runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, Api.CreateContextRequest(contextId)) =>
|
||||
(requestId, contextId)
|
||||
case msg =>
|
||||
fail(s"Unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.CreateContextResponse(contextId)
|
||||
)
|
||||
client.expectJson(json.executionContextCreateResponse(1, contextId))
|
||||
|
||||
// attach visualization
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val expressionId = UUID.randomUUID()
|
||||
val config =
|
||||
VisualizationConfiguration(contextId, "Test.Main", ".to_json.to_text")
|
||||
client.send(
|
||||
json.executionContextExecuteExpressionRequest(
|
||||
2,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
config
|
||||
)
|
||||
)
|
||||
val requestId2 =
|
||||
runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(
|
||||
requestId,
|
||||
Api.AttachVisualization(
|
||||
`visualizationId`,
|
||||
`expressionId`,
|
||||
_
|
||||
)
|
||||
) =>
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId2,
|
||||
Api.ModuleNotFound(config.visualizationModule)
|
||||
)
|
||||
client.expectJson(
|
||||
json.executionContextModuleNotFound(
|
||||
2,
|
||||
config.visualizationModule
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"successfully attach visualization" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
|
@ -7,6 +7,8 @@ import org.enso.languageserver.runtime.{
|
||||
VisualizationExpression
|
||||
}
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
object ExecutionContextJsonMessages {
|
||||
|
||||
def localCall(expressionId: Api.ExpressionId) =
|
||||
@ -109,67 +111,23 @@ object ExecutionContextJsonMessages {
|
||||
|
||||
def executionContextExecuteExpressionRequest(
|
||||
reqId: Int,
|
||||
executionContextId: UUID,
|
||||
visualizationId: Api.VisualizationId,
|
||||
expressionId: Api.ExpressionId,
|
||||
configuration: VisualizationConfiguration
|
||||
expression: String
|
||||
) =
|
||||
configuration.expression match {
|
||||
case VisualizationExpression.Text(module, expression) =>
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "executionContext/executeExpression",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"visualizationId": $visualizationId,
|
||||
"expressionId": $expressionId,
|
||||
"visualizationConfig": {
|
||||
"executionContextId": ${configuration.executionContextId},
|
||||
"visualizationModule": $module,
|
||||
"expression": $expression
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
case VisualizationExpression.ModuleMethod(methodPointer, Vector()) =>
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "executionContext/executeExpression",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"visualizationId": $visualizationId,
|
||||
"expressionId": $expressionId,
|
||||
"visualizationConfig": {
|
||||
"executionContextId": ${configuration.executionContextId},
|
||||
"expression": {
|
||||
"module": ${methodPointer.module},
|
||||
"definedOnType": ${methodPointer.definedOnType},
|
||||
"name": ${methodPointer.name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
case VisualizationExpression.ModuleMethod(methodPointer, arguments) =>
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "executionContext/executeExpression",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"visualizationId": $visualizationId,
|
||||
"expressionId": $expressionId,
|
||||
"visualizationConfig": {
|
||||
"executionContextId": ${configuration.executionContextId},
|
||||
"expression": {
|
||||
"module": ${methodPointer.module},
|
||||
"definedOnType": ${methodPointer.definedOnType},
|
||||
"name": ${methodPointer.name}
|
||||
},
|
||||
"positionalArgumentsExpressions": $arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "executionContext/executeExpression",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"executionContextId": $executionContextId,
|
||||
"visualizationId": $visualizationId,
|
||||
"expressionId": $expressionId,
|
||||
"expression": $expression
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def executionContextAttachVisualizationRequest(
|
||||
reqId: Int,
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.enso.polyglot.debugger;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* The result of executed oneshot visualization expression.
|
||||
*
|
||||
* @param result the execution result. {@code null} if the execution resulted in exception.
|
||||
* @param error the execution error. {@code null} if the execution was successful.
|
||||
* @param visualizationId the visualization id.
|
||||
* @param expressionId the id of expression that provides the execution scope.
|
||||
* @param expressionValue the value of the expression that provides the execution scope.
|
||||
*/
|
||||
public record ExecutedVisualization(
|
||||
Object result,
|
||||
Throwable error,
|
||||
UUID visualizationId,
|
||||
UUID expressionId,
|
||||
Object expressionValue) {}
|
@ -9,36 +9,60 @@ import java.util.UUID;
|
||||
public interface IdExecutionService {
|
||||
String INSTRUMENT_ID = "id-value-extractor";
|
||||
|
||||
public abstract class Info {
|
||||
|
||||
/** @return UUID of the node, never {@code null}. */
|
||||
public abstract UUID getId();
|
||||
|
||||
/** @return associated result or {@code null} if there is no associated result. */
|
||||
public abstract Object getResult();
|
||||
|
||||
/** @return {@code true} when the result is panic, {@code false} otherwise. */
|
||||
public abstract boolean isPanic();
|
||||
|
||||
/**
|
||||
* @return time (in nanoseconds) needed to compute the result or {@code -1} when not available.
|
||||
*/
|
||||
public abstract long getElapsedTime();
|
||||
|
||||
/**
|
||||
* Evaluates given code in the context of current UUID location.
|
||||
*
|
||||
* @param code the Enso code to evaluate.
|
||||
* @return result of the evaluation.
|
||||
*/
|
||||
public abstract Object eval(String code);
|
||||
}
|
||||
|
||||
public interface Callbacks {
|
||||
|
||||
/**
|
||||
* Finds out previously computed result for given id. If a result is returned, then the
|
||||
* execution of given node is skipped and the value is returned back.
|
||||
*
|
||||
* @param nodeId identification of the node to be computed
|
||||
* @param info info with UUID the node to be computed
|
||||
* @return {@code null} should the execution of the node be performed; any other value to skip
|
||||
* the execution and return the value as a result.
|
||||
*/
|
||||
Object findCachedResult(UUID nodeId);
|
||||
Object findCachedResult(Info info);
|
||||
|
||||
/**
|
||||
* Notifies when an execution of a node is over.
|
||||
*
|
||||
* @param nodeId identification of the node to be computed
|
||||
* @param result the just computed result
|
||||
* @param isPanic was the result a panic?
|
||||
* @param nanoElapsedTime how long it took to compute the result?
|
||||
* @param info info with node id, {@link Info#getResult()}, {@link Info#isPanic()} and {@link
|
||||
* Info#getElapsedTime()}
|
||||
*/
|
||||
void updateCachedResult(UUID nodeId, Object result, boolean isPanic, long nanoElapsedTime);
|
||||
void updateCachedResult(Info info);
|
||||
|
||||
/**
|
||||
* Notification when a returned value is a function.
|
||||
*
|
||||
* @param nodeId identification of the node to be computed
|
||||
* @param result info about function call
|
||||
* @param info with identification of the node and {@link Info#getResult()} info about function
|
||||
* call
|
||||
* @return {@code null} should the execution of the node be performed; any other value to skip
|
||||
* the execution and return the value as a result.
|
||||
*/
|
||||
Object onFunctionReturn(UUID nodeId, TruffleObject result);
|
||||
Object onFunctionReturn(Info info);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,6 +128,10 @@ object Runtime {
|
||||
value = classOf[Api.AttachVisualization],
|
||||
name = "attachVisualization"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.ExecuteExpression],
|
||||
name = "executeExpression"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.VisualizationAttached],
|
||||
name = "visualizationAttached"
|
||||
@ -1561,6 +1565,13 @@ object Runtime {
|
||||
*/
|
||||
final case class InitializedNotification() extends ApiResponse
|
||||
|
||||
final case class ExecuteExpression(
|
||||
contextId: ContextId,
|
||||
visualizationId: VisualizationId,
|
||||
expressionId: ExpressionId,
|
||||
expression: String
|
||||
) extends ApiRequest
|
||||
|
||||
/** A request sent from the client to the runtime server, to create a new
|
||||
* visualization for an expression identified by `expressionId`.
|
||||
*
|
||||
|
@ -67,7 +67,7 @@ class Compiler(
|
||||
private val importResolver: ImportResolver = new ImportResolver(this)
|
||||
private val irCachingEnabled = !context.isIrCachingDisabled
|
||||
private val useGlobalCacheLocations = context.isUseGlobalCacheLocations
|
||||
private val isInteractiveMode = context.isInteractiveMode()
|
||||
private val isInteractiveMode = context.isInteractiveMode
|
||||
private val output: PrintStream =
|
||||
if (config.outputRedirect.isDefined)
|
||||
new PrintStream(config.outputRedirect.get)
|
||||
@ -111,7 +111,7 @@ class Compiler(
|
||||
}
|
||||
|
||||
/** @return the package repository instance. */
|
||||
def getPackageRepository(): PackageRepository =
|
||||
def getPackageRepository: PackageRepository =
|
||||
context.getPackageRepository
|
||||
|
||||
/** Processes the provided language sources, registering any bindings in the
|
||||
@ -141,7 +141,7 @@ class Compiler(
|
||||
shouldCompileDependencies: Boolean,
|
||||
useGlobalCacheLocations: Boolean
|
||||
): Future[java.lang.Boolean] = {
|
||||
getPackageRepository().getMainProjectPackage match {
|
||||
getPackageRepository.getMainProjectPackage match {
|
||||
case None =>
|
||||
context.log(
|
||||
Level.SEVERE,
|
||||
@ -268,7 +268,7 @@ class Compiler(
|
||||
) {
|
||||
val importedModulesLoadedFromSource = importedModules
|
||||
.filter(isLoadedFromSource)
|
||||
.map(context.getModuleName(_))
|
||||
.map(context.getModuleName)
|
||||
context.log(
|
||||
Compiler.defaultLogLevel,
|
||||
"{0} imported module caches were invalided, forcing invalidation of {1}. [{2}]",
|
||||
@ -278,7 +278,7 @@ class Compiler(
|
||||
importedModulesLoadedFromSource.take(10).mkString("", ",", "...")
|
||||
)
|
||||
)
|
||||
context.updateModule(module, _.invalidateCache)
|
||||
context.updateModule(module, _.invalidateCache())
|
||||
parseModule(module)
|
||||
runImportsAndExportsResolution(module, generateCode)
|
||||
} else {
|
||||
@ -457,9 +457,9 @@ class Compiler(
|
||||
private def isModuleInRootPackage(module: Module): Boolean = {
|
||||
if (!context.isInteractive(module)) {
|
||||
val pkg = PackageRepositoryUtils
|
||||
.getPackageOf(getPackageRepository(), module.getSourceFile)
|
||||
.getPackageOf(getPackageRepository, module.getSourceFile)
|
||||
.toScala
|
||||
pkg.contains(getPackageRepository().getMainProjectPackage.get)
|
||||
pkg.contains(getPackageRepository.getMainProjectPackage.get)
|
||||
} else false
|
||||
}
|
||||
|
||||
@ -572,7 +572,7 @@ class Compiler(
|
||||
"Parsing module [{0}].",
|
||||
context.getModuleName(module)
|
||||
)
|
||||
context.updateModule(module, _.resetScope)
|
||||
context.updateModule(module, _.resetScope())
|
||||
|
||||
if (irCachingEnabled && !context.isInteractive(module)) {
|
||||
if (context.deserializeModule(this, module)) {
|
||||
@ -603,7 +603,7 @@ class Compiler(
|
||||
"Loading module [{0}] from source.",
|
||||
context.getModuleName(module)
|
||||
)
|
||||
context.updateModule(module, _.resetScope)
|
||||
context.updateModule(module, _.resetScope())
|
||||
|
||||
val moduleContext = ModuleContext(
|
||||
module = module,
|
||||
@ -694,10 +694,10 @@ class Compiler(
|
||||
.build()
|
||||
val tree = ensoCompiler.parse(source.getCharacters)
|
||||
|
||||
ensoCompiler.generateIRInline(tree).flatMap { ir =>
|
||||
ensoCompiler.generateIRInline(tree).map { ir =>
|
||||
val compilerOutput = runCompilerPhasesInline(ir, newContext)
|
||||
runErrorHandlingInline(compilerOutput, source, newContext)
|
||||
Some((newContext, compilerOutput, source))
|
||||
(newContext, compilerOutput, source)
|
||||
}
|
||||
}
|
||||
|
||||
@ -742,14 +742,6 @@ class Compiler(
|
||||
def parseInline(source: Source): Tree =
|
||||
ensoCompiler.parse(source.getCharacters())
|
||||
|
||||
/** Parses the metadata of the provided language sources.
|
||||
*
|
||||
* @param source the code to parse
|
||||
* @return the source metadata
|
||||
*/
|
||||
// def parseMeta(source: CharSequence): IDMap =
|
||||
// Parser().splitMeta(source.toString)._2
|
||||
|
||||
/** Enhances the provided IR with import/export statements for the provided list
|
||||
* of fully qualified names of modules. The statements are considered to be "synthetic" i.e. compiler-generated.
|
||||
* That way one can access modules using fully qualified names.
|
||||
@ -858,7 +850,7 @@ class Compiler(
|
||||
* for inline evaluation
|
||||
* @return the output result of the
|
||||
*/
|
||||
def runCompilerPhasesInline(
|
||||
private def runCompilerPhasesInline(
|
||||
ir: Expression,
|
||||
inlineContext: InlineContext
|
||||
): Expression = {
|
||||
@ -872,12 +864,12 @@ class Compiler(
|
||||
* @param source the original source code.
|
||||
* @param inlineContext the inline compilation context.
|
||||
*/
|
||||
def runErrorHandlingInline(
|
||||
private def runErrorHandlingInline(
|
||||
ir: Expression,
|
||||
source: Source,
|
||||
inlineContext: InlineContext
|
||||
): Unit =
|
||||
if (config.isStrictErrors) {
|
||||
if (inlineContext.compilerConfig.isStrictErrors) {
|
||||
val errors = GatherDiagnostics
|
||||
.runExpression(ir, inlineContext)
|
||||
.unsafeGetMetadata(
|
||||
@ -895,7 +887,7 @@ class Compiler(
|
||||
*
|
||||
* @param modules the modules to check against errors
|
||||
*/
|
||||
def runErrorHandling(
|
||||
private def runErrorHandling(
|
||||
modules: List[Module]
|
||||
): Unit = {
|
||||
if (config.isStrictErrors) {
|
||||
@ -921,7 +913,7 @@ class Compiler(
|
||||
* @param module the module for which to gather diagnostics
|
||||
* @return the diagnostics from the module
|
||||
*/
|
||||
def gatherDiagnostics(module: Module): List[Diagnostic] = {
|
||||
private def gatherDiagnostics(module: Module): List[Diagnostic] = {
|
||||
GatherDiagnostics
|
||||
.runModule(
|
||||
context.getIr(module),
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.compiler.data
|
||||
|
||||
import org.enso.compiler.{PackageRepository}
|
||||
import org.enso.compiler.PackageRepository
|
||||
import org.enso.compiler.PackageRepository.ModuleMap
|
||||
import org.enso.compiler.context.CompilerContext.Module
|
||||
import org.enso.compiler.core.Implicits.AsMetadata
|
||||
@ -58,7 +58,7 @@ case class BindingsMap(
|
||||
override def restoreFromSerialization(
|
||||
compiler: Compiler
|
||||
): Option[BindingsMap] = {
|
||||
val packageRepository = compiler.getPackageRepository()
|
||||
val packageRepository = compiler.getPackageRepository
|
||||
this.toConcrete(packageRepository.getModuleMap)
|
||||
}
|
||||
|
||||
@ -1012,7 +1012,7 @@ object BindingsMap {
|
||||
override def restoreFromSerialization(
|
||||
compiler: Compiler
|
||||
): Option[Resolution] = {
|
||||
val moduleMap = compiler.getPackageRepository().getModuleMap
|
||||
val moduleMap = compiler.getPackageRepository.getModuleMap
|
||||
this.target.toConcrete(moduleMap).map(t => this.copy(target = t))
|
||||
}
|
||||
|
||||
|
@ -459,7 +459,7 @@ case object FullyQualifiedNames extends IRPass {
|
||||
override def restoreFromSerialization(
|
||||
compiler: CompilerContext
|
||||
): Option[PartiallyResolvedFQN] = {
|
||||
val packageRepository = compiler.getPackageRepository()
|
||||
val packageRepository = compiler.getPackageRepository
|
||||
moduleRef
|
||||
.toConcrete(packageRepository.getModuleMap)
|
||||
.map(ResolvedModule(_))
|
||||
|
@ -0,0 +1,50 @@
|
||||
package org.enso.interpreter.instrument.command;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.enso.interpreter.instrument.execution.RuntimeContext;
|
||||
import org.enso.interpreter.instrument.job.ExecuteExpressionJob;
|
||||
import org.enso.interpreter.instrument.job.ExecuteJob;
|
||||
import org.enso.polyglot.runtime.Runtime$Api$VisualizationAttached;
|
||||
import scala.Option;
|
||||
import scala.concurrent.ExecutionContext;
|
||||
import scala.concurrent.Future;
|
||||
import scala.runtime.BoxedUnit;
|
||||
|
||||
/** The command that handles the execute expression request. */
|
||||
public final class ExecuteExpressionCommand extends ContextCmd {
|
||||
|
||||
private final UUID contextId;
|
||||
private final UUID visualizationId;
|
||||
private final UUID expressionId;
|
||||
private final String expression;
|
||||
|
||||
/**
|
||||
* Create the {@link ExecuteExpressionCommand}.
|
||||
*
|
||||
* @param maybeRequestId the request id.
|
||||
* @param contextId the execution context id.
|
||||
* @param visualizationId the visualization id.
|
||||
* @param expressionId the expression providing the execution scope.
|
||||
* @param expression the expression to execute.
|
||||
*/
|
||||
public ExecuteExpressionCommand(
|
||||
Option<UUID> maybeRequestId,
|
||||
UUID contextId,
|
||||
UUID visualizationId,
|
||||
UUID expressionId,
|
||||
String expression) {
|
||||
super(contextId, maybeRequestId);
|
||||
this.contextId = contextId;
|
||||
this.visualizationId = visualizationId;
|
||||
this.expressionId = expressionId;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<BoxedUnit> executeCmd(RuntimeContext ctx, ExecutionContext ec) {
|
||||
reply(new Runtime$Api$VisualizationAttached(), ctx);
|
||||
return ctx.jobProcessor()
|
||||
.run(new ExecuteExpressionJob(contextId, visualizationId, expressionId, expression))
|
||||
.flatMap(executable -> ctx.jobProcessor().run(ExecuteJob.apply(executable)), ec);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.enso.interpreter.instrument.job;
|
||||
|
||||
import com.oracle.truffle.api.TruffleLogger;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import org.enso.interpreter.instrument.Visualization;
|
||||
import org.enso.interpreter.instrument.execution.Executable;
|
||||
import org.enso.interpreter.instrument.execution.RuntimeContext;
|
||||
import org.enso.interpreter.util.ScalaConversions;
|
||||
|
||||
/** The job that schedules the execution of the expression. */
|
||||
public class ExecuteExpressionJob extends Job<Executable> {
|
||||
|
||||
private final UUID contextId;
|
||||
private final UUID visualizationId;
|
||||
private final UUID expressionId;
|
||||
private final String expression;
|
||||
|
||||
/**
|
||||
* Create the {@link ExecuteExpressionJob}.
|
||||
*
|
||||
* @param contextId the execution context id.
|
||||
* @param visualizationId the visualization id.
|
||||
* @param expressionId the expression providing the execution scope.
|
||||
* @param expression the expression to execute.
|
||||
*/
|
||||
public ExecuteExpressionJob(
|
||||
UUID contextId, UUID visualizationId, UUID expressionId, String expression) {
|
||||
super(ScalaConversions.cons(contextId, ScalaConversions.nil()), false, false);
|
||||
this.contextId = contextId;
|
||||
this.visualizationId = visualizationId;
|
||||
this.expressionId = expressionId;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executable run(RuntimeContext ctx) {
|
||||
TruffleLogger logger = ctx.executionService().getLogger();
|
||||
long lockTimestamp = ctx.locking().acquireContextLock(contextId);
|
||||
|
||||
try {
|
||||
Visualization visualization =
|
||||
new Visualization.OneshotExpression(visualizationId, expressionId, contextId, expression);
|
||||
ctx.contextManager().upsertVisualization(contextId, visualization);
|
||||
|
||||
var stack = ctx.contextManager().getStack(contextId);
|
||||
return new Executable(contextId, stack);
|
||||
} finally {
|
||||
ctx.locking().releaseContextLock(contextId);
|
||||
logger.log(
|
||||
Level.FINEST,
|
||||
"Kept context lock [{0}] for {1} milliseconds.",
|
||||
new Object[] {
|
||||
this.getClass().getSimpleName(), System.currentTimeMillis() - lockTimestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package org.enso.interpreter.service;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
import org.enso.interpreter.instrument.MethodCallsCache;
|
||||
import org.enso.interpreter.instrument.RuntimeCache;
|
||||
import org.enso.interpreter.instrument.UpdatesSynchronizationState;
|
||||
import org.enso.interpreter.instrument.Visualization;
|
||||
import org.enso.interpreter.instrument.VisualizationHolder;
|
||||
import org.enso.interpreter.instrument.profiling.ExecutionTime;
|
||||
import org.enso.interpreter.instrument.profiling.ProfilingInfo;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
@ -19,12 +20,13 @@ import org.enso.interpreter.runtime.type.Constants;
|
||||
import org.enso.interpreter.service.ExecutionService.ExpressionCall;
|
||||
import org.enso.interpreter.service.ExecutionService.ExpressionValue;
|
||||
import org.enso.interpreter.service.ExecutionService.FunctionCallInfo;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.interop.TruffleObject;
|
||||
import org.enso.polyglot.debugger.ExecutedVisualization;
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
import scala.collection.Iterator;
|
||||
|
||||
final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
|
||||
private final VisualizationHolder visualizationHolder;
|
||||
private final UUID nextExecutionItem;
|
||||
private final RuntimeCache cache;
|
||||
private final MethodCallsCache methodCallsCache;
|
||||
@ -33,8 +35,10 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
private final Consumer<ExpressionValue> onCachedCallback;
|
||||
private final Consumer<ExpressionValue> onComputedCallback;
|
||||
private final Consumer<ExpressionCall> functionCallCallback;
|
||||
private final Consumer<ExecutedVisualization> onExecutedVisualizationCallback;
|
||||
|
||||
/** Creates callbacks instance.
|
||||
/**
|
||||
* Creates callbacks instance.
|
||||
*
|
||||
* @param cache the precomputed expression values.
|
||||
* @param methodCallsCache the storage tracking the executed updateCachedResult calls.
|
||||
@ -45,11 +49,16 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
* @param onCachedCallback the consumer of the cached value events.
|
||||
*/
|
||||
ExecutionCallbacks(
|
||||
UUID nextExecutionItem,
|
||||
RuntimeCache cache, MethodCallsCache methodCallsCache, UpdatesSynchronizationState syncState,
|
||||
Consumer<ExpressionValue> onCachedCallback, Consumer<ExpressionValue> onComputedCallback,
|
||||
Consumer<ExpressionCall> functionCallCallback
|
||||
) {
|
||||
VisualizationHolder visualizationHolder,
|
||||
UUID nextExecutionItem,
|
||||
RuntimeCache cache,
|
||||
MethodCallsCache methodCallsCache,
|
||||
UpdatesSynchronizationState syncState,
|
||||
Consumer<ExpressionValue> onCachedCallback,
|
||||
Consumer<ExpressionValue> onComputedCallback,
|
||||
Consumer<ExpressionCall> functionCallCallback,
|
||||
Consumer<ExecutedVisualization> onExecutedVisualizationCallback) {
|
||||
this.visualizationHolder = visualizationHolder;
|
||||
this.nextExecutionItem = nextExecutionItem;
|
||||
this.cache = cache;
|
||||
this.methodCallsCache = methodCallsCache;
|
||||
@ -57,46 +66,46 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
this.onCachedCallback = onCachedCallback;
|
||||
this.onComputedCallback = onComputedCallback;
|
||||
this.functionCallCallback = functionCallCallback;
|
||||
this.onExecutedVisualizationCallback = onExecutedVisualizationCallback;
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
public final Object findCachedResult(UUID nodeId) {
|
||||
// Add a flag to say it was cached.
|
||||
// An array of `ProfilingInfo` in the value update.
|
||||
Object result = cache.get(nodeId);
|
||||
@Override
|
||||
public Object findCachedResult(IdExecutionService.Info info) {
|
||||
UUID nodeId = info.getId();
|
||||
Object result = getCachedResult(nodeId);
|
||||
|
||||
if (result != null) {
|
||||
executeOneshotExpressions(nodeId, result, info);
|
||||
}
|
||||
|
||||
// When executing the call stack we need to capture the FunctionCall of the next (top) stack
|
||||
// item in the `functionCallCallback`. We allow to execute the cached `stackTop` value to be
|
||||
// able to continue the stack execution, and unwind later from the `onReturnValue` callback.
|
||||
if (result != null && !nodeId.equals(nextExecutionItem)) {
|
||||
var value = new ExpressionValue(
|
||||
nodeId,
|
||||
result,
|
||||
cache.getType(nodeId),
|
||||
typeOf(result),
|
||||
calls.get(nodeId),
|
||||
cache.getCall(nodeId),
|
||||
new ProfilingInfo[]{ExecutionTime.empty()},
|
||||
true
|
||||
);
|
||||
onCachedCallback.accept(value);
|
||||
callOnCachedCallback(nodeId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
public final void updateCachedResult(UUID nodeId, Object result, boolean isPanic, long nanoTimeElapsed) {
|
||||
@Override
|
||||
public void updateCachedResult(IdExecutionService.Info info) {
|
||||
Object result = info.getResult();
|
||||
String resultType = typeOf(result);
|
||||
UUID nodeId = info.getId();
|
||||
String cachedType = cache.getType(nodeId);
|
||||
FunctionCallInfo call = functionCallInfoById(nodeId);
|
||||
FunctionCallInfo cachedCall = cache.getCall(nodeId);
|
||||
ProfilingInfo[] profilingInfo = new ProfilingInfo[]{new ExecutionTime(nanoTimeElapsed)};
|
||||
ProfilingInfo[] profilingInfo = new ProfilingInfo[] {new ExecutionTime(info.getElapsedTime())};
|
||||
|
||||
ExpressionValue expressionValue
|
||||
= new ExpressionValue(nodeId, result, resultType, cachedType, call, cachedCall, profilingInfo, false);
|
||||
ExpressionValue expressionValue =
|
||||
new ExpressionValue(
|
||||
nodeId, result, resultType, cachedType, call, cachedCall, profilingInfo, false);
|
||||
syncState.setExpressionUnsync(nodeId);
|
||||
syncState.setVisualizationUnsync(nodeId);
|
||||
|
||||
boolean isPanic = info.isPanic();
|
||||
// Panics are not cached because a panic can be fixed by changing seemingly unrelated code,
|
||||
// like imports, and the invalidation mechanism can not always track those changes and
|
||||
// appropriately invalidate all dependent expressions.
|
||||
@ -106,7 +115,8 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
}
|
||||
cache.putType(nodeId, resultType);
|
||||
|
||||
passExpressionValueToCallback(expressionValue);
|
||||
callOnComputedCallback(expressionValue);
|
||||
executeOneshotExpressions(nodeId, result, info);
|
||||
if (isPanic) {
|
||||
// We mark the node as executed so that it is not reported as not executed call after the
|
||||
// program execution is complete. If we clear the call from the cache instead, it will mess
|
||||
@ -116,8 +126,11 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
public final Object onFunctionReturn(UUID nodeId, TruffleObject result) {
|
||||
var fnCall = (FunctionCallInstrumentationNode.FunctionCall) result;
|
||||
@Override
|
||||
public Object onFunctionReturn(IdExecutionService.Info info) {
|
||||
FunctionCallInstrumentationNode.FunctionCall fnCall =
|
||||
(FunctionCallInstrumentationNode.FunctionCall) info.getResult();
|
||||
UUID nodeId = info.getId();
|
||||
calls.put(nodeId, FunctionCallInfo.fromFunctionCall(fnCall));
|
||||
functionCallCallback.accept(new ExpressionCall(nodeId, fnCall));
|
||||
// Return cached value after capturing the enterable function call in `functionCallCallback`
|
||||
@ -130,10 +143,63 @@ final class ExecutionCallbacks implements IdExecutionService.Callbacks {
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private void passExpressionValueToCallback(ExpressionValue expressionValue) {
|
||||
private void callOnComputedCallback(ExpressionValue expressionValue) {
|
||||
onComputedCallback.accept(expressionValue);
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private void callOnCachedCallback(UUID nodeId, Object result) {
|
||||
ExpressionValue expressionValue =
|
||||
new ExpressionValue(
|
||||
nodeId,
|
||||
result,
|
||||
cache.getType(nodeId),
|
||||
typeOf(result),
|
||||
calls.get(nodeId),
|
||||
cache.getCall(nodeId),
|
||||
new ProfilingInfo[] {ExecutionTime.empty()},
|
||||
true);
|
||||
|
||||
onCachedCallback.accept(expressionValue);
|
||||
}
|
||||
|
||||
private void executeOneshotExpressions(UUID nodeId, Object result, IdExecutionService.Info info) {
|
||||
Iterator<Visualization> visualizations = findVisualizations(nodeId);
|
||||
while (visualizations.hasNext()) {
|
||||
Visualization visualization = visualizations.next();
|
||||
|
||||
if (visualization instanceof Visualization.OneshotExpression oneshotExpression) {
|
||||
Object visualizationResult = null;
|
||||
Throwable visualizationError = null;
|
||||
try {
|
||||
visualizationResult = info.eval(oneshotExpression.expression());
|
||||
} catch (Exception exception) {
|
||||
visualizationError = exception;
|
||||
}
|
||||
|
||||
ExecutedVisualization executedVisualization =
|
||||
new ExecutedVisualization(
|
||||
visualizationResult, visualizationError, visualization.id(), nodeId, result);
|
||||
callOnExecutedVisualizationCallback(executedVisualization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private void callOnExecutedVisualizationCallback(ExecutedVisualization executedVisualization) {
|
||||
onExecutedVisualizationCallback.accept(executedVisualization);
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private Object getCachedResult(UUID nodeId) {
|
||||
return cache.get(nodeId);
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private Iterator<Visualization> findVisualizations(UUID nodeId) {
|
||||
return visualizationHolder.find(nodeId).iterator();
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private FunctionCallInfo functionCallInfoById(UUID nodeId) {
|
||||
return calls.get(nodeId);
|
||||
|
@ -1,62 +1,5 @@
|
||||
package org.enso.interpreter.service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.oracle.truffle.api.CallTarget;
|
||||
import com.oracle.truffle.api.instrumentation.EventBinding;
|
||||
import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory;
|
||||
import com.oracle.truffle.api.nodes.RootNode;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.enso.interpreter.instrument.profiling.ProfilingInfo;
|
||||
import org.enso.interpreter.node.MethodRootNode;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.node.expression.atom.QualifiedAccessorNode;
|
||||
import org.enso.interpreter.node.expression.builtin.BuiltinRootNode;
|
||||
import org.enso.interpreter.runtime.Module;
|
||||
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
|
||||
import org.enso.interpreter.runtime.data.Type;
|
||||
import org.enso.logger.masking.MaskedString;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
|
||||
import org.enso.compiler.context.SimpleUpdate;
|
||||
import org.enso.interpreter.instrument.Endpoint;
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
import org.enso.interpreter.instrument.MethodCallsCache;
|
||||
import org.enso.interpreter.instrument.NotificationHandler;
|
||||
import org.enso.interpreter.instrument.RuntimeCache;
|
||||
import org.enso.interpreter.instrument.Timer;
|
||||
import org.enso.interpreter.instrument.UpdatesSynchronizationState;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.node.expression.builtin.text.util.TypeToDisplayTextNodeGen;
|
||||
import org.enso.interpreter.runtime.EnsoContext;
|
||||
import org.enso.interpreter.runtime.Module;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.data.Type;
|
||||
import org.enso.interpreter.runtime.error.PanicException;
|
||||
import org.enso.interpreter.runtime.scope.ModuleScope;
|
||||
import org.enso.interpreter.runtime.state.State;
|
||||
import org.enso.interpreter.service.error.FailedToApplyEditsException;
|
||||
import org.enso.interpreter.service.error.MethodNotFoundException;
|
||||
import org.enso.interpreter.service.error.ModuleNotFoundException;
|
||||
import org.enso.interpreter.service.error.SourceNotFoundException;
|
||||
import org.enso.interpreter.service.error.TypeNotFoundException;
|
||||
import org.enso.lockmanager.client.ConnectedLockManager;
|
||||
import org.enso.polyglot.LanguageInfo;
|
||||
import org.enso.polyglot.MethodNames;
|
||||
import org.enso.text.editing.JavaEditorAdapter;
|
||||
import org.enso.text.editing.model;
|
||||
|
||||
import com.oracle.truffle.api.CallTarget;
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.TruffleLogger;
|
||||
@ -72,6 +15,53 @@ import com.oracle.truffle.api.nodes.Node;
|
||||
import com.oracle.truffle.api.nodes.RootNode;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.enso.compiler.context.SimpleUpdate;
|
||||
import org.enso.interpreter.instrument.Endpoint;
|
||||
import org.enso.interpreter.instrument.MethodCallsCache;
|
||||
import org.enso.interpreter.instrument.NotificationHandler;
|
||||
import org.enso.interpreter.instrument.RuntimeCache;
|
||||
import org.enso.interpreter.instrument.Timer;
|
||||
import org.enso.interpreter.instrument.UpdatesSynchronizationState;
|
||||
import org.enso.interpreter.instrument.VisualizationHolder;
|
||||
import org.enso.interpreter.instrument.profiling.ProfilingInfo;
|
||||
import org.enso.interpreter.node.MethodRootNode;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.node.expression.atom.QualifiedAccessorNode;
|
||||
import org.enso.interpreter.node.expression.builtin.BuiltinRootNode;
|
||||
import org.enso.interpreter.node.expression.builtin.text.util.TypeToDisplayTextNodeGen;
|
||||
import org.enso.interpreter.runtime.EnsoContext;
|
||||
import org.enso.interpreter.runtime.Module;
|
||||
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
|
||||
import org.enso.interpreter.runtime.data.Type;
|
||||
import org.enso.interpreter.runtime.error.PanicException;
|
||||
import org.enso.interpreter.runtime.scope.ModuleScope;
|
||||
import org.enso.interpreter.runtime.state.State;
|
||||
import org.enso.interpreter.service.error.FailedToApplyEditsException;
|
||||
import org.enso.interpreter.service.error.MethodNotFoundException;
|
||||
import org.enso.interpreter.service.error.ModuleNotFoundException;
|
||||
import org.enso.interpreter.service.error.SourceNotFoundException;
|
||||
import org.enso.interpreter.service.error.TypeNotFoundException;
|
||||
import org.enso.lockmanager.client.ConnectedLockManager;
|
||||
import org.enso.logger.masking.MaskedString;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
import org.enso.polyglot.LanguageInfo;
|
||||
import org.enso.polyglot.MethodNames;
|
||||
import org.enso.polyglot.debugger.ExecutedVisualization;
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
import org.enso.text.editing.JavaEditorAdapter;
|
||||
import org.enso.text.editing.model;
|
||||
|
||||
/**
|
||||
* A service allowing externally-triggered code execution, registered by an instance of the
|
||||
* language.
|
||||
@ -87,7 +77,6 @@ public final class ExecutionService {
|
||||
private final ExecuteRootNode execute = new ExecuteRootNode();
|
||||
private final CallRootNode call = new CallRootNode();
|
||||
private final InvokeMemberRootNode invoke = new InvokeMemberRootNode();
|
||||
|
||||
private final Timer timer;
|
||||
|
||||
/**
|
||||
@ -168,6 +157,7 @@ public final class ExecutionService {
|
||||
* @param onCachedCallback the consumer of the cached value events.
|
||||
*/
|
||||
public void execute(
|
||||
VisualizationHolder visualizationHolder,
|
||||
Module module,
|
||||
FunctionCallInstrumentationNode.FunctionCall call,
|
||||
RuntimeCache cache,
|
||||
@ -176,23 +166,32 @@ public final class ExecutionService {
|
||||
UUID nextExecutionItem,
|
||||
Consumer<ExecutionService.ExpressionCall> funCallCallback,
|
||||
Consumer<ExecutionService.ExpressionValue> onComputedCallback,
|
||||
Consumer<ExecutionService.ExpressionValue> onCachedCallback
|
||||
) throws ArityException, SourceNotFoundException, UnsupportedMessageException, UnsupportedTypeException {
|
||||
Consumer<ExecutionService.ExpressionValue> onCachedCallback,
|
||||
Consumer<ExecutedVisualization> onExecutedVisualizationCallback)
|
||||
throws ArityException,
|
||||
SourceNotFoundException,
|
||||
UnsupportedMessageException,
|
||||
UnsupportedTypeException {
|
||||
SourceSection src = call.getFunction().getSourceSection();
|
||||
if (src == null) {
|
||||
throw new SourceNotFoundException(call.getFunction().getName());
|
||||
}
|
||||
var callbacks = new ExecutionCallbacks(
|
||||
nextExecutionItem, cache, methodCallsCache, syncState,
|
||||
onCachedCallback, onComputedCallback, funCallCallback
|
||||
);
|
||||
var callbacks =
|
||||
new ExecutionCallbacks(
|
||||
visualizationHolder,
|
||||
nextExecutionItem,
|
||||
cache,
|
||||
methodCallsCache,
|
||||
syncState,
|
||||
onCachedCallback,
|
||||
onComputedCallback,
|
||||
funCallCallback,
|
||||
onExecutedVisualizationCallback);
|
||||
Optional<EventBinding<ExecutionEventNodeFactory>> eventNodeFactory =
|
||||
idExecutionInstrument.map(service -> service.bind(
|
||||
module,
|
||||
call.getFunction().getCallTarget(),
|
||||
callbacks,
|
||||
this.timer
|
||||
));
|
||||
idExecutionInstrument.map(
|
||||
service ->
|
||||
service.bind(
|
||||
module, call.getFunction().getCallTarget(), callbacks, this.timer));
|
||||
Object p = context.getThreadManager().enter();
|
||||
try {
|
||||
execute.getCallTarget().call(call);
|
||||
@ -221,22 +220,27 @@ public final class ExecutionService {
|
||||
String moduleName,
|
||||
String typeName,
|
||||
String methodName,
|
||||
VisualizationHolder visualizationHolder,
|
||||
RuntimeCache cache,
|
||||
MethodCallsCache methodCallsCache,
|
||||
UpdatesSynchronizationState syncState,
|
||||
UUID nextExecutionItem,
|
||||
Consumer<
|
||||
ExecutionService.ExpressionCall> funCallCallback,
|
||||
Consumer<ExecutionService.ExpressionCall> funCallCallback,
|
||||
Consumer<ExecutionService.ExpressionValue> onComputedCallback,
|
||||
Consumer<ExecutionService.ExpressionValue> onCachedCallback
|
||||
)
|
||||
throws ArityException, TypeNotFoundException, MethodNotFoundException,
|
||||
ModuleNotFoundException, UnsupportedMessageException, UnsupportedTypeException {
|
||||
Consumer<ExecutionService.ExpressionValue> onCachedCallback,
|
||||
Consumer<ExecutedVisualization> onExecutedVisualizationCallback)
|
||||
throws ArityException,
|
||||
TypeNotFoundException,
|
||||
MethodNotFoundException,
|
||||
ModuleNotFoundException,
|
||||
UnsupportedMessageException,
|
||||
UnsupportedTypeException {
|
||||
Module module =
|
||||
context.findModule(moduleName).orElseThrow(() -> new ModuleNotFoundException(moduleName));
|
||||
FunctionCallInstrumentationNode.FunctionCall call =
|
||||
prepareFunctionCall(module, typeName, methodName);
|
||||
execute(
|
||||
visualizationHolder,
|
||||
module,
|
||||
call,
|
||||
cache,
|
||||
@ -245,20 +249,18 @@ public final class ExecutionService {
|
||||
nextExecutionItem,
|
||||
funCallCallback,
|
||||
onComputedCallback,
|
||||
onCachedCallback
|
||||
);
|
||||
onCachedCallback,
|
||||
onExecutedVisualizationCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an expression in the scope of the provided module.
|
||||
*
|
||||
* @param module the module providing a scope for the expression
|
||||
* @param expression the expression to evaluated
|
||||
* @param expression the expression to evaluate
|
||||
* @return a result of evaluation
|
||||
*/
|
||||
public Object evaluateExpression(Module module, String expression)
|
||||
throws UnsupportedMessageException, ArityException, UnknownIdentifierException,
|
||||
UnsupportedTypeException {
|
||||
public Object evaluateExpression(Module module, String expression) {
|
||||
Object p = context.getThreadManager().enter();
|
||||
try {
|
||||
return invoke.getCallTarget().call(module, expression);
|
||||
@ -290,11 +292,10 @@ public final class ExecutionService {
|
||||
* @param argument the argument applied to the function
|
||||
* @return the result of calling the function
|
||||
*/
|
||||
public Object callFunction(Object fn, Object argument)
|
||||
throws UnsupportedTypeException, ArityException, UnsupportedMessageException {
|
||||
public Object callFunction(Object fn, Object argument) {
|
||||
Object p = context.getThreadManager().enter();
|
||||
try {
|
||||
return call.getCallTarget().call(fn, new Object[] { argument });
|
||||
return call.getCallTarget().call(fn, new Object[] {argument});
|
||||
} finally {
|
||||
context.getThreadManager().leave(p);
|
||||
}
|
||||
@ -310,8 +311,11 @@ public final class ExecutionService {
|
||||
* @return the result of calling the function
|
||||
*/
|
||||
public Object callFunctionWithInstrument(
|
||||
RuntimeCache cache, Module module, Object function, Object... arguments)
|
||||
throws UnsupportedTypeException, ArityException, UnsupportedMessageException {
|
||||
VisualizationHolder visualizationHolder,
|
||||
RuntimeCache cache,
|
||||
Module module,
|
||||
Object function,
|
||||
Object... arguments) {
|
||||
UUID nextExecutionItem = null;
|
||||
CallTarget entryCallTarget =
|
||||
(function instanceof Function) ? ((Function) function).getCallTarget() : null;
|
||||
@ -322,18 +326,22 @@ public final class ExecutionService {
|
||||
(value) -> context.getLogger().finest("_ON_COMPUTED " + value.getExpressionId());
|
||||
Consumer<ExpressionValue> onCachedCallback =
|
||||
(value) -> context.getLogger().finest("_ON_CACHED_VALUE " + value.getExpressionId());
|
||||
Consumer<ExecutedVisualization> onExecutedVisualizationCallback = (value) -> {};
|
||||
|
||||
var callbacks = new ExecutionCallbacks(
|
||||
nextExecutionItem, cache, methodCallsCache, syncState,
|
||||
onCachedCallback, onComputedCallback, funCallCallback
|
||||
);
|
||||
var callbacks =
|
||||
new ExecutionCallbacks(
|
||||
visualizationHolder,
|
||||
nextExecutionItem,
|
||||
cache,
|
||||
methodCallsCache,
|
||||
syncState,
|
||||
onCachedCallback,
|
||||
onComputedCallback,
|
||||
funCallCallback,
|
||||
onExecutedVisualizationCallback);
|
||||
Optional<EventBinding<ExecutionEventNodeFactory>> eventNodeFactory =
|
||||
idExecutionInstrument.map(service -> service.bind(
|
||||
module,
|
||||
entryCallTarget,
|
||||
callbacks,
|
||||
this.timer
|
||||
));
|
||||
idExecutionInstrument.map(
|
||||
service -> service.bind(module, entryCallTarget, callbacks, this.timer));
|
||||
Object p = context.getThreadManager().enter();
|
||||
try {
|
||||
return call.getCallTarget().call(function, arguments);
|
||||
@ -451,12 +459,11 @@ public final class ExecutionService {
|
||||
var iop = InteropLibrary.getUncached();
|
||||
var p = context.getThreadManager().enter();
|
||||
try {
|
||||
// Invoking a member on an Atom that does not have a method `to_display_text` will not, contrary to what is
|
||||
// Invoking a member on an Atom that does not have a method `to_display_text` will not contrary to what is
|
||||
// expected from the documentation, throw an `UnsupportedMessageException`.
|
||||
// Instead it will crash with some internal assertion deep inside runtime. Hence the check.
|
||||
if (iop.isMemberInvocable(panic.getPayload(), "to_display_text")) {
|
||||
return iop.asString(
|
||||
iop.invokeMember(panic.getPayload(), "to_display_text"));
|
||||
return iop.asString(iop.invokeMember(panic.getPayload(), "to_display_text"));
|
||||
} else throw UnsupportedMessageException.create();
|
||||
} catch (UnsupportedMessageException
|
||||
| ArityException
|
||||
@ -530,11 +537,15 @@ public final class ExecutionService {
|
||||
|
||||
@Override
|
||||
public Object execute(VirtualFrame frame) {
|
||||
var module = frame.getArguments()[0];
|
||||
var expression = frame.getArguments()[1];
|
||||
Object[] arguments = frame.getArguments();
|
||||
Object module = arguments[0];
|
||||
Object expression = arguments[1];
|
||||
try {
|
||||
return iop.invokeMember(module, MethodNames.Module.EVAL_EXPRESSION, expression);
|
||||
} catch (UnknownIdentifierException | UnsupportedTypeException | ArityException | UnsupportedMessageException ex) {
|
||||
} catch (UnknownIdentifierException
|
||||
| UnsupportedTypeException
|
||||
| ArityException
|
||||
| UnsupportedMessageException ex) {
|
||||
throw raise(RuntimeException.class, ex);
|
||||
}
|
||||
}
|
||||
@ -686,7 +697,8 @@ public final class ExecutionService {
|
||||
}
|
||||
|
||||
/** Points to the definition of a runtime function. */
|
||||
public record FunctionPointer(QualifiedName moduleName, QualifiedName typeName, String functionName) {
|
||||
public record FunctionPointer(
|
||||
QualifiedName moduleName, QualifiedName typeName, String functionName) {
|
||||
|
||||
public static FunctionPointer fromFunction(Function function) {
|
||||
RootNode rootNode = function.getCallTarget().getRootNode();
|
||||
@ -753,8 +765,8 @@ public final class ExecutionService {
|
||||
return false;
|
||||
}
|
||||
FunctionCallInfo that = (FunctionCallInfo) o;
|
||||
return Objects.equals(functionPointer, that.functionPointer) && Arrays.equals(
|
||||
notAppliedArguments, that.notAppliedArguments);
|
||||
return Objects.equals(functionPointer, that.functionPointer)
|
||||
&& Arrays.equals(notAppliedArguments, that.notAppliedArguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -768,14 +780,16 @@ public final class ExecutionService {
|
||||
*
|
||||
* @param call the function call.
|
||||
*/
|
||||
public static FunctionCallInfo fromFunctionCall(FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
public static FunctionCallInfo fromFunctionCall(
|
||||
FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
FunctionPointer functionPointer = FunctionPointer.fromFunction(call.getFunction());
|
||||
int[] notAppliedArguments = collectNotAppliedArguments(call);
|
||||
|
||||
return new FunctionCallInfo(functionPointer, notAppliedArguments);
|
||||
}
|
||||
|
||||
private static int[] collectNotAppliedArguments(FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
private static int[] collectNotAppliedArguments(
|
||||
FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
Object[] arguments = call.getArguments();
|
||||
int[] notAppliedArgs = new int[arguments.length];
|
||||
int notAppliedArgsSize = 0;
|
||||
|
@ -153,8 +153,9 @@ object CacheInvalidation {
|
||||
command: Command,
|
||||
indexes: Set[IndexSelector] = Set()
|
||||
): Unit =
|
||||
visualizations.foreach { visualization =>
|
||||
run(visualization.cache, command, indexes)
|
||||
visualizations.collect {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
run(visualization.cache, command, indexes)
|
||||
}
|
||||
|
||||
/** Run a cache invalidation instruction on an execution stack.
|
||||
|
@ -34,18 +34,6 @@ class ExecutionContextManager {
|
||||
contexts -= id
|
||||
}
|
||||
|
||||
/** Gets a context with a given id.
|
||||
*
|
||||
* @param id the context id.
|
||||
* @return the context with the given id, if exists.
|
||||
*/
|
||||
def get(id: ContextId): Option[ContextId] =
|
||||
synchronized {
|
||||
for {
|
||||
_ <- contexts.get(id)
|
||||
} yield id
|
||||
}
|
||||
|
||||
/** Gets a stack for a given context id.
|
||||
*
|
||||
* @param id the context id.
|
||||
@ -116,6 +104,16 @@ class ExecutionContextManager {
|
||||
state.visualizations.upsert(visualization)
|
||||
}
|
||||
|
||||
/** Gets a context with a given id.
|
||||
*
|
||||
* @param id the context id.
|
||||
* @return the context with the given id, if exists.
|
||||
*/
|
||||
def getVisualizationHolder(id: ContextId): VisualizationHolder =
|
||||
synchronized {
|
||||
contexts.get(id).map(_.visualizations).getOrElse(new VisualizationHolder)
|
||||
}
|
||||
|
||||
/** Get visualizations of all execution contexts. */
|
||||
def getAllVisualizations: Iterable[Visualization] =
|
||||
synchronized {
|
||||
|
@ -2,25 +2,47 @@ package org.enso.interpreter.instrument
|
||||
|
||||
import org.enso.interpreter.runtime.Module
|
||||
import org.enso.polyglot.runtime.Runtime.Api.{
|
||||
ContextId,
|
||||
ExpressionId,
|
||||
VisualizationConfiguration,
|
||||
VisualizationId
|
||||
}
|
||||
|
||||
/** An object containing visualization data.
|
||||
*
|
||||
* @param id the unique identifier of visualization
|
||||
* @param expressionId the identifier of expression that the visualization is
|
||||
* attached to
|
||||
* @param callback the callable expression used to generate visualization data
|
||||
*/
|
||||
case class Visualization(
|
||||
id: VisualizationId,
|
||||
expressionId: ExpressionId,
|
||||
cache: RuntimeCache,
|
||||
module: Module,
|
||||
config: VisualizationConfiguration,
|
||||
visualizationExpressionId: Option[ExpressionId],
|
||||
callback: AnyRef,
|
||||
arguments: Vector[AnyRef]
|
||||
)
|
||||
sealed trait Visualization {
|
||||
def id: VisualizationId
|
||||
def expressionId: ExpressionId
|
||||
}
|
||||
object Visualization {
|
||||
|
||||
/** An object containing visualization data.
|
||||
*
|
||||
* @param id the unique identifier of visualization
|
||||
* @param expressionId the identifier of expression that the visualization is
|
||||
* attached to
|
||||
* @param callback the callable expression used to generate visualization data
|
||||
*/
|
||||
case class AttachedVisualization(
|
||||
id: VisualizationId,
|
||||
expressionId: ExpressionId,
|
||||
cache: RuntimeCache,
|
||||
module: Module,
|
||||
config: VisualizationConfiguration,
|
||||
visualizationExpressionId: Option[ExpressionId],
|
||||
callback: AnyRef,
|
||||
arguments: Vector[AnyRef]
|
||||
) extends Visualization
|
||||
|
||||
/** An expression that will be executed in the local scope.
|
||||
*
|
||||
* @param id the unique identifier of visualization
|
||||
* @param expressionId the identifier of expression that provides the execution scope
|
||||
* @param executionContextId the identifier of the execution context
|
||||
* @param expression the expression to execute
|
||||
*/
|
||||
case class OneshotExpression(
|
||||
id: VisualizationId,
|
||||
expressionId: ExpressionId,
|
||||
executionContextId: ContextId,
|
||||
expression: String
|
||||
) extends Visualization
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import scala.collection.mutable
|
||||
|
||||
/** A mutable holder of all visualizations attached to an execution context.
|
||||
*/
|
||||
class VisualizationHolder() {
|
||||
class VisualizationHolder {
|
||||
|
||||
private val visualizationMap: mutable.Map[ExpressionId, List[Visualization]] =
|
||||
mutable.Map.empty.withDefaultValue(List.empty)
|
||||
@ -50,8 +50,14 @@ class VisualizationHolder() {
|
||||
* @param module the qualified module name
|
||||
* @return a list of matching visualization
|
||||
*/
|
||||
def findByModule(module: QualifiedName): Iterable[Visualization] =
|
||||
visualizationMap.values.flatten.filter(_.module.getName == module)
|
||||
def findByModule(
|
||||
module: QualifiedName
|
||||
): Iterable[Visualization.AttachedVisualization] =
|
||||
visualizationMap.values.flatten.collect {
|
||||
case visualization: Visualization.AttachedVisualization
|
||||
if visualization.module.getName == module =>
|
||||
visualization
|
||||
}
|
||||
|
||||
/** Returns a visualization with the provided id.
|
||||
*
|
||||
@ -69,6 +75,6 @@ class VisualizationHolder() {
|
||||
object VisualizationHolder {
|
||||
|
||||
/** Returns an empty visualization holder. */
|
||||
def empty = new VisualizationHolder()
|
||||
def empty = new VisualizationHolder
|
||||
|
||||
}
|
||||
|
@ -37,6 +37,15 @@ object CommandFactory {
|
||||
case payload: Api.AttachVisualization =>
|
||||
new AttachVisualizationCmd(request.requestId, payload)
|
||||
|
||||
case payload: Api.ExecuteExpression =>
|
||||
new ExecuteExpressionCommand(
|
||||
request.requestId,
|
||||
payload.contextId,
|
||||
payload.visualizationId,
|
||||
payload.expressionId,
|
||||
payload.expression
|
||||
)
|
||||
|
||||
case payload: Api.DetachVisualization =>
|
||||
new DetachVisualizationCmd(request.requestId, payload)
|
||||
|
||||
|
@ -508,10 +508,14 @@ final class EnsureCompiledJob(
|
||||
|
||||
private def getCacheMetadata(
|
||||
visualization: Visualization
|
||||
): Option[CachePreferenceAnalysis.Metadata] = {
|
||||
val module = visualization.module
|
||||
module.getIr.getMetadata(CachePreferenceAnalysis)
|
||||
}
|
||||
): Option[CachePreferenceAnalysis.Metadata] =
|
||||
visualization match {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
val module = visualization.module
|
||||
module.getIr.getMetadata(CachePreferenceAnalysis)
|
||||
case _: Visualization.OneshotExpression =>
|
||||
None
|
||||
}
|
||||
|
||||
/** Get all project modules in the current compiler scope. */
|
||||
private def getProjectModulesInScope(implicit
|
||||
|
@ -2,11 +2,7 @@ package org.enso.interpreter.instrument.job
|
||||
|
||||
import cats.implicits._
|
||||
import com.oracle.truffle.api.exception.AbstractTruffleException
|
||||
import org.enso.interpreter.service.ExecutionService.{
|
||||
ExpressionCall,
|
||||
ExpressionValue,
|
||||
FunctionPointer
|
||||
}
|
||||
import org.enso.interpreter.instrument._
|
||||
import org.enso.interpreter.instrument.execution.{
|
||||
Completion,
|
||||
ErrorResolver,
|
||||
@ -14,7 +10,6 @@ import org.enso.interpreter.instrument.execution.{
|
||||
RuntimeContext
|
||||
}
|
||||
import org.enso.interpreter.instrument.profiling.ExecutionTime
|
||||
import org.enso.interpreter.instrument._
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode.FunctionCall
|
||||
import org.enso.interpreter.node.expression.builtin.meta.TypeOfNode
|
||||
import org.enso.interpreter.runtime.`type`.{Types, TypesGen}
|
||||
@ -26,8 +21,14 @@ import org.enso.interpreter.runtime.error.{
|
||||
WarningsLibrary,
|
||||
WithWarnings
|
||||
}
|
||||
import org.enso.interpreter.service.ExecutionService.{
|
||||
ExpressionCall,
|
||||
ExpressionValue,
|
||||
FunctionPointer
|
||||
}
|
||||
import org.enso.interpreter.service.error._
|
||||
import org.enso.polyglot.LanguageInfo
|
||||
import org.enso.polyglot.debugger.ExecutedVisualization
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.polyglot.runtime.Runtime.Api.{ContextId, ExecutionResult}
|
||||
|
||||
@ -92,23 +93,60 @@ object ProgramExecutionSupport {
|
||||
cache,
|
||||
syncState
|
||||
) =>
|
||||
val onExecutedVisualizationCallback: Consumer[ExecutedVisualization] = {
|
||||
executedVisualization =>
|
||||
val visualizationResult =
|
||||
Either.cond(
|
||||
executedVisualization.error() eq null,
|
||||
executedVisualization.result(),
|
||||
executedVisualization.error()
|
||||
)
|
||||
sendVisualizationUpdate(
|
||||
visualizationResult,
|
||||
contextId,
|
||||
syncState,
|
||||
executedVisualization.visualizationId(),
|
||||
executedVisualization.expressionId(),
|
||||
executedVisualization.expressionValue()
|
||||
)
|
||||
}
|
||||
|
||||
ctx.executionService.execute(
|
||||
module.toString,
|
||||
cons.item,
|
||||
function,
|
||||
ctx.contextManager.getVisualizationHolder(contextId),
|
||||
cache,
|
||||
methodCallsCache,
|
||||
syncState,
|
||||
callStack.headOption.map(_.expressionId).orNull,
|
||||
callablesCallback,
|
||||
onComputedValueCallback,
|
||||
onCachedValueCallback
|
||||
onCachedValueCallback,
|
||||
onExecutedVisualizationCallback
|
||||
)
|
||||
case ExecutionFrame(
|
||||
ExecutionItem.CallData(expressionId, callData),
|
||||
cache,
|
||||
syncState
|
||||
) =>
|
||||
val onExecutedVisualizationCallback: Consumer[ExecutedVisualization] = {
|
||||
executedVisualization =>
|
||||
val visualizationResult =
|
||||
Either.cond(
|
||||
executedVisualization.error() eq null,
|
||||
executedVisualization.result(),
|
||||
executedVisualization.error()
|
||||
)
|
||||
sendVisualizationUpdate(
|
||||
visualizationResult,
|
||||
contextId,
|
||||
syncState,
|
||||
executedVisualization.visualizationId(),
|
||||
executedVisualization.expressionId(),
|
||||
executedVisualization.expressionValue()
|
||||
)
|
||||
}
|
||||
val module =
|
||||
ctx.executionService.getContext
|
||||
.findModuleByExpressionId(expressionId)
|
||||
@ -116,6 +154,7 @@ object ProgramExecutionSupport {
|
||||
new ModuleNotFoundForExpressionIdException(expressionId)
|
||||
)
|
||||
ctx.executionService.execute(
|
||||
ctx.contextManager.getVisualizationHolder(contextId),
|
||||
module,
|
||||
callData,
|
||||
cache,
|
||||
@ -124,7 +163,8 @@ object ProgramExecutionSupport {
|
||||
callStack.headOption.map(_.expressionId).orNull,
|
||||
callablesCallback,
|
||||
onComputedValueCallback,
|
||||
onCachedValueCallback
|
||||
onCachedValueCallback,
|
||||
onExecutedVisualizationCallback
|
||||
)
|
||||
}
|
||||
|
||||
@ -417,7 +457,6 @@ object ProgramExecutionSupport {
|
||||
* @param value the computed value
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
@com.oracle.truffle.api.CompilerDirectives.TruffleBoundary
|
||||
private def sendVisualizationUpdates(
|
||||
contextId: ContextId,
|
||||
syncState: UpdatesSynchronizationState,
|
||||
@ -429,70 +468,82 @@ object ProgramExecutionSupport {
|
||||
contextId,
|
||||
value.getExpressionId
|
||||
)
|
||||
visualizations.foreach { visualization =>
|
||||
sendVisualizationUpdate(
|
||||
contextId,
|
||||
syncState,
|
||||
visualization,
|
||||
value.getExpressionId,
|
||||
value.getValue
|
||||
)
|
||||
visualizations.collect {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
executeAndSendVisualizationUpdate(
|
||||
contextId,
|
||||
syncState,
|
||||
visualization,
|
||||
value.getExpressionId,
|
||||
value.getValue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def executeVisualization(
|
||||
contextId: ContextId,
|
||||
visualization: Visualization.AttachedVisualization,
|
||||
expressionId: UUID,
|
||||
expressionValue: AnyRef
|
||||
)(implicit ctx: RuntimeContext): Either[Throwable, AnyRef] =
|
||||
Either
|
||||
.catchNonFatal {
|
||||
val logger = ctx.executionService.getLogger
|
||||
logger.log(
|
||||
Level.FINEST,
|
||||
"Executing visualization [{0}] on expression [{1}] of [{2}]...",
|
||||
Array[Object](
|
||||
visualization.id,
|
||||
expressionId,
|
||||
Try(TypeOfNode.getUncached.execute(expressionValue))
|
||||
.getOrElse(expressionValue.getClass)
|
||||
)
|
||||
)
|
||||
ctx.executionService.callFunctionWithInstrument(
|
||||
ctx.contextManager.getVisualizationHolder(contextId),
|
||||
visualization.cache,
|
||||
visualization.module,
|
||||
visualization.callback,
|
||||
expressionValue +: visualization.arguments: _*
|
||||
)
|
||||
}
|
||||
|
||||
/** Compute the visualization of the expression value and send an update.
|
||||
*
|
||||
* @param contextId an identifier of an execution context
|
||||
* @param visualization the visualization data
|
||||
* @param visualizationId the id of the visualization
|
||||
* @param expressionId the id of expression to visualise
|
||||
* @param expressionValue the value of expression to visualise
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
def sendVisualizationUpdate(
|
||||
private def sendVisualizationUpdate(
|
||||
visualizationResult: Either[Throwable, AnyRef],
|
||||
contextId: ContextId,
|
||||
syncState: UpdatesSynchronizationState,
|
||||
visualization: Visualization,
|
||||
visualizationId: UUID,
|
||||
expressionId: UUID,
|
||||
expressionValue: AnyRef
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
val errorOrVisualizationData =
|
||||
Either
|
||||
.catchNonFatal {
|
||||
ctx.executionService.getLogger.log(
|
||||
Level.FINEST,
|
||||
"Executing visualization [{0}] on expression [{1}] of [{2}]...",
|
||||
Array[Object](
|
||||
visualization.config,
|
||||
expressionId,
|
||||
Try(TypeOfNode.getUncached.execute(expressionValue))
|
||||
.getOrElse(expressionValue.getClass)
|
||||
)
|
||||
)
|
||||
ctx.executionService.callFunctionWithInstrument(
|
||||
visualization.cache,
|
||||
visualization.module,
|
||||
visualization.callback,
|
||||
expressionValue +: visualization.arguments: _*
|
||||
)
|
||||
}
|
||||
.flatMap(visualizationResultToBytes)
|
||||
val result = errorOrVisualizationData match {
|
||||
val result = visualizationResultToBytes(visualizationResult) match {
|
||||
case Left(_: ThreadInterruptedException) =>
|
||||
Completion.Interrupted
|
||||
|
||||
case Left(error) =>
|
||||
val message =
|
||||
Option(error.getMessage).getOrElse(error.getClass.getSimpleName)
|
||||
val typeOfNode = Try(TypeOfNode.getUncached.execute(expressionValue))
|
||||
if (!typeOfNode.map(TypesGen.isPanicSentinel).getOrElse(false)) {
|
||||
if (!TypesGen.isPanicSentinel(expressionValue)) {
|
||||
val typeOfNode =
|
||||
Option(TypeOfNode.getUncached.execute(expressionValue))
|
||||
.getOrElse(expressionValue.getClass)
|
||||
ctx.executionService.getLogger.log(
|
||||
Level.WARNING,
|
||||
"Execution of visualization [{0}] on value [{1}] of [{2}] failed.",
|
||||
"Execution of visualization [{0}] on value [{1}] of [{2}] failed. {3}",
|
||||
Array[Object](
|
||||
visualization.config,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
typeOfNode.getOrElse(expressionValue.getClass),
|
||||
typeOfNode,
|
||||
message,
|
||||
error
|
||||
)
|
||||
)
|
||||
@ -501,7 +552,7 @@ object ProgramExecutionSupport {
|
||||
Api.Response(
|
||||
Api.VisualizationEvaluationFailed(
|
||||
contextId,
|
||||
visualization.id,
|
||||
visualizationId,
|
||||
expressionId,
|
||||
message,
|
||||
getDiagnosticOutcome.lift(error)
|
||||
@ -520,7 +571,7 @@ object ProgramExecutionSupport {
|
||||
Api.Response(
|
||||
Api.VisualizationUpdate(
|
||||
Api.VisualizationContext(
|
||||
visualization.id,
|
||||
visualizationId,
|
||||
contextId,
|
||||
expressionId
|
||||
),
|
||||
@ -535,20 +586,56 @@ object ProgramExecutionSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute the visualization of the expression value and send an update.
|
||||
*
|
||||
* @param contextId an identifier of an execution context
|
||||
* @param visualization the visualization data
|
||||
* @param expressionId the id of expression to visualise
|
||||
* @param expressionValue the value of expression to visualise
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
def executeAndSendVisualizationUpdate(
|
||||
contextId: ContextId,
|
||||
syncState: UpdatesSynchronizationState,
|
||||
visualization: Visualization,
|
||||
expressionId: UUID,
|
||||
expressionValue: AnyRef
|
||||
)(implicit ctx: RuntimeContext): Unit =
|
||||
visualization match {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
val visualizationResult = executeVisualization(
|
||||
contextId,
|
||||
visualization,
|
||||
expressionId,
|
||||
expressionValue
|
||||
)
|
||||
sendVisualizationUpdate(
|
||||
visualizationResult,
|
||||
contextId,
|
||||
syncState,
|
||||
visualization.id,
|
||||
expressionId,
|
||||
expressionValue
|
||||
)
|
||||
case _: Visualization.OneshotExpression =>
|
||||
}
|
||||
|
||||
/** Convert the result of Enso visualization function to a byte array.
|
||||
*
|
||||
* @param value the result of Enso visualization function
|
||||
* @param visualizationResult the result of Enso visualization function
|
||||
* @return either a byte array representing the visualization result or an
|
||||
* error
|
||||
*/
|
||||
private def visualizationResultToBytes(
|
||||
value: AnyRef
|
||||
): Either[VisualizationException, Array[Byte]] = {
|
||||
Option(VisualizationResult.visualizationResultToBytes(value)).toRight(
|
||||
new VisualizationException(
|
||||
s"Cannot encode ${value.getClass} to byte array."
|
||||
visualizationResult: Either[Throwable, AnyRef]
|
||||
): Either[Throwable, Array[Byte]] = {
|
||||
visualizationResult.flatMap { value =>
|
||||
Option(VisualizationResult.visualizationResultToBytes(value)).toRight(
|
||||
new VisualizationException(
|
||||
s"Cannot encode ${value.getClass} to byte array."
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract the method call information from the provided expression value.
|
||||
|
@ -79,7 +79,7 @@ class UpsertVisualizationJob(
|
||||
|
||||
case Right(EvaluationResult(module, callable, arguments)) =>
|
||||
val visualization =
|
||||
UpsertVisualizationJob.updateVisualization(
|
||||
UpsertVisualizationJob.updateAttachedVisualization(
|
||||
visualizationId,
|
||||
expressionId,
|
||||
module,
|
||||
@ -96,7 +96,7 @@ class UpsertVisualizationJob(
|
||||
)
|
||||
cachedValue match {
|
||||
case Some(value) =>
|
||||
ProgramExecutionSupport.sendVisualizationUpdate(
|
||||
ProgramExecutionSupport.executeAndSendVisualizationUpdate(
|
||||
config.executionContextId,
|
||||
stack.headOption.get.syncState,
|
||||
visualization,
|
||||
@ -185,30 +185,39 @@ object UpsertVisualizationJob {
|
||||
*/
|
||||
def upsertVisualization(
|
||||
visualization: Visualization
|
||||
)(implicit ctx: RuntimeContext, logger: TruffleLogger): Unit = {
|
||||
val visualizationConfig = visualization.config
|
||||
val expressionId = visualization.expressionId
|
||||
val visualizationId = visualization.id
|
||||
val maybeCallable =
|
||||
evaluateVisualizationExpression(
|
||||
visualizationConfig.visualizationModule,
|
||||
visualizationConfig.expression
|
||||
)
|
||||
)(implicit ctx: RuntimeContext, logger: TruffleLogger): Unit =
|
||||
visualization match {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
val visualizationConfig = visualization.config
|
||||
val expressionId = visualization.expressionId
|
||||
val visualizationId = visualization.id
|
||||
val maybeCallable =
|
||||
evaluateVisualizationExpression(
|
||||
visualizationConfig.visualizationModule,
|
||||
visualizationConfig.expression
|
||||
)
|
||||
|
||||
maybeCallable.foreach { result =>
|
||||
updateAttachedVisualization(
|
||||
visualizationId,
|
||||
expressionId,
|
||||
result.module,
|
||||
visualizationConfig,
|
||||
result.callback,
|
||||
result.arguments
|
||||
)
|
||||
val stack =
|
||||
ctx.contextManager.getStack(visualizationConfig.executionContextId)
|
||||
requireVisualizationSynchronization(stack, expressionId)
|
||||
}
|
||||
|
||||
case visualization: Visualization.OneshotExpression =>
|
||||
ctx.contextManager.upsertVisualization(
|
||||
visualization.executionContextId,
|
||||
visualization
|
||||
)
|
||||
|
||||
maybeCallable.foreach { result =>
|
||||
updateVisualization(
|
||||
visualizationId,
|
||||
expressionId,
|
||||
result.module,
|
||||
visualizationConfig,
|
||||
result.callback,
|
||||
result.arguments
|
||||
)
|
||||
val stack =
|
||||
ctx.contextManager.getStack(visualizationConfig.executionContextId)
|
||||
requireVisualizationSynchronization(stack, expressionId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Find module by name.
|
||||
*
|
||||
@ -462,7 +471,7 @@ object UpsertVisualizationJob {
|
||||
* @param ctx the runtime context
|
||||
* @return the re-evaluated visualization
|
||||
*/
|
||||
private def updateVisualization(
|
||||
private def updateAttachedVisualization(
|
||||
visualizationId: Api.VisualizationId,
|
||||
expressionId: Api.ExpressionId,
|
||||
module: Module,
|
||||
@ -472,16 +481,17 @@ object UpsertVisualizationJob {
|
||||
)(implicit ctx: RuntimeContext, logger: TruffleLogger): Visualization = {
|
||||
val visualizationExpressionId =
|
||||
findVisualizationExpressionId(module, visualizationConfig.expression)
|
||||
val visualization = Visualization(
|
||||
visualizationId,
|
||||
expressionId,
|
||||
new RuntimeCache(),
|
||||
module,
|
||||
visualizationConfig,
|
||||
visualizationExpressionId,
|
||||
callback,
|
||||
arguments
|
||||
)
|
||||
val visualization =
|
||||
Visualization.AttachedVisualization(
|
||||
visualizationId,
|
||||
expressionId,
|
||||
new RuntimeCache(),
|
||||
module,
|
||||
visualizationConfig,
|
||||
visualizationExpressionId,
|
||||
callback,
|
||||
arguments
|
||||
)
|
||||
val writeLockTimestamp = ctx.locking.acquireWriteCompilationLock()
|
||||
try {
|
||||
invalidateCaches(visualization)
|
||||
@ -575,15 +585,19 @@ object UpsertVisualizationJob {
|
||||
*
|
||||
* @param visualization the visualization to update
|
||||
*/
|
||||
private def setCacheWeights(visualization: Visualization): Unit = {
|
||||
visualization.module.getIr.getMetadata(CachePreferenceAnalysis).foreach {
|
||||
metadata =>
|
||||
CacheInvalidation.runVisualizations(
|
||||
Seq(visualization),
|
||||
CacheInvalidation.Command.SetMetadata(metadata)
|
||||
)
|
||||
private def setCacheWeights(visualization: Visualization): Unit =
|
||||
visualization match {
|
||||
case visualization: Visualization.AttachedVisualization =>
|
||||
visualization.module.getIr
|
||||
.getMetadata(CachePreferenceAnalysis)
|
||||
.foreach { metadata =>
|
||||
CacheInvalidation.runVisualizations(
|
||||
Seq(visualization),
|
||||
CacheInvalidation.Command.SetMetadata(metadata)
|
||||
)
|
||||
}
|
||||
case _: Visualization.OneshotExpression =>
|
||||
}
|
||||
}
|
||||
|
||||
/** Invalidate the first cached dependent node of the provided expression.
|
||||
*
|
||||
|
@ -1,8 +1,5 @@
|
||||
package org.enso.interpreter.instrument;
|
||||
|
||||
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
|
||||
import com.oracle.truffle.api.CallTarget;
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.RootCallTarget;
|
||||
@ -10,6 +7,7 @@ import com.oracle.truffle.api.Truffle;
|
||||
import com.oracle.truffle.api.exception.AbstractTruffleException;
|
||||
import com.oracle.truffle.api.frame.FrameInstance;
|
||||
import com.oracle.truffle.api.frame.FrameInstanceVisitor;
|
||||
import com.oracle.truffle.api.frame.MaterializedFrame;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
import com.oracle.truffle.api.instrumentation.EventBinding;
|
||||
import com.oracle.truffle.api.instrumentation.EventContext;
|
||||
@ -20,16 +18,18 @@ import com.oracle.truffle.api.instrumentation.StandardTags;
|
||||
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
|
||||
import com.oracle.truffle.api.interop.InteropException;
|
||||
import com.oracle.truffle.api.interop.InteropLibrary;
|
||||
import com.oracle.truffle.api.interop.TruffleObject;
|
||||
import com.oracle.truffle.api.nodes.Node;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.enso.interpreter.node.ClosureRootNode;
|
||||
import org.enso.interpreter.node.EnsoRootNode;
|
||||
import org.enso.interpreter.node.ExpressionNode;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.node.expression.debug.EvalNode;
|
||||
import org.enso.interpreter.runtime.EnsoContext;
|
||||
import org.enso.interpreter.runtime.Module;
|
||||
import org.enso.interpreter.runtime.callable.CallerInfo;
|
||||
import org.enso.interpreter.runtime.callable.function.Function;
|
||||
import org.enso.interpreter.runtime.control.TailCallException;
|
||||
import org.enso.interpreter.runtime.data.text.Text;
|
||||
@ -39,8 +39,7 @@ import org.enso.interpreter.runtime.error.PanicSentinel;
|
||||
import org.enso.interpreter.runtime.state.State;
|
||||
import org.enso.interpreter.runtime.tag.AvoidIdInstrumentationTag;
|
||||
import org.enso.interpreter.runtime.tag.IdentifiedTag;
|
||||
|
||||
import com.oracle.truffle.api.interop.TruffleObject;
|
||||
import org.enso.polyglot.debugger.IdExecutionService;
|
||||
|
||||
/** An instrument for getting values from AST-identified expressions. */
|
||||
@TruffleInstrument.Registration(
|
||||
@ -68,6 +67,8 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
private final Callbacks callbacks;
|
||||
private final Timer timer;
|
||||
|
||||
private final EvalNode evalNode = EvalNode.build();
|
||||
|
||||
/**
|
||||
* Creates a new event node factory.
|
||||
*
|
||||
@ -75,11 +76,7 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
* @param callbacks communication with users
|
||||
* @param timer the timer for timing execution
|
||||
*/
|
||||
IdEventNodeFactory(
|
||||
CallTarget entryCallTarget,
|
||||
Callbacks callbacks,
|
||||
Timer timer
|
||||
) {
|
||||
IdEventNodeFactory(CallTarget entryCallTarget, Callbacks callbacks, Timer timer) {
|
||||
this.entryCallTarget = entryCallTarget;
|
||||
this.callbacks = callbacks;
|
||||
this.timer = timer;
|
||||
@ -90,9 +87,97 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
return new IdExecutionEventNode(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The execution event node class used by this instrument.
|
||||
*/
|
||||
/** Implementation of {@link Info} for the instrumented {@link Node}. */
|
||||
private final class NodeInfo extends Info {
|
||||
|
||||
private final UUID nodeId;
|
||||
private final Object result;
|
||||
private final long elapsedTime;
|
||||
private final MaterializedFrame materializedFrame;
|
||||
private final EnsoRootNode ensoRootNode;
|
||||
|
||||
/**
|
||||
* Create a {@link NodeInfo} for the entered node.
|
||||
*
|
||||
* @param materializedFrame the execution frame
|
||||
* @param node the entered node
|
||||
*/
|
||||
public NodeInfo(
|
||||
MaterializedFrame materializedFrame,
|
||||
Node node) {
|
||||
super();
|
||||
|
||||
this.nodeId = getNodeId(node);
|
||||
this.result = null;
|
||||
this.elapsedTime = -1;
|
||||
this.materializedFrame = materializedFrame;
|
||||
this.ensoRootNode = (EnsoRootNode) node.getRootNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link NodeInfo} for the executed node.
|
||||
*
|
||||
* @param nodeId the id of the executed node
|
||||
* @param result the result of the node execution
|
||||
* @param elapsedTime the execution time
|
||||
* @param materializedFrame the execution frame
|
||||
* @param node the executed node
|
||||
*/
|
||||
public NodeInfo(
|
||||
UUID nodeId,
|
||||
Object result,
|
||||
long elapsedTime,
|
||||
MaterializedFrame materializedFrame,
|
||||
Node node) {
|
||||
super();
|
||||
|
||||
this.nodeId = nodeId;
|
||||
this.result = result;
|
||||
this.elapsedTime = elapsedTime;
|
||||
this.materializedFrame = materializedFrame;
|
||||
this.ensoRootNode = (EnsoRootNode) node.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPanic() {
|
||||
return result instanceof AbstractTruffleException && !(result instanceof DataflowError);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getElapsedTime() {
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object eval(String code) {
|
||||
CallerInfo callerInfo =
|
||||
new CallerInfo(
|
||||
materializedFrame, ensoRootNode.getLocalScope(), ensoRootNode.getModuleScope());
|
||||
|
||||
return evalNode.execute(callerInfo, State.create(EnsoContext.get(null)), Text.create(code));
|
||||
}
|
||||
|
||||
private static UUID getNodeId(Node node) {
|
||||
return switch (node) {
|
||||
case ExpressionNode n -> n.getId();
|
||||
case FunctionCallInstrumentationNode n -> n.getId();
|
||||
case null -> null;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** The execution event node class used by this instrument. */
|
||||
private class IdExecutionEventNode extends ExecutionEventNode {
|
||||
|
||||
private final EventContext context;
|
||||
@ -117,13 +202,10 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
if (!isTopFrame(entryCallTarget)) {
|
||||
return;
|
||||
}
|
||||
onEnterImpl();
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private void onEnterImpl() {
|
||||
UUID nodeId = getNodeId(context.getInstrumentedNode());
|
||||
var result = callbacks.findCachedResult(nodeId);
|
||||
Info info = new NodeInfo(frame.materialize(), context.getInstrumentedNode());
|
||||
Object result = callbacks.findCachedResult(info);
|
||||
|
||||
if (result != null) {
|
||||
throw context.createUnwind(result);
|
||||
}
|
||||
@ -131,12 +213,11 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a node (either a function call sentry or an identified
|
||||
* expression) finishes execution.
|
||||
* Triggered when a node (either a function call sentry or an identified expression) finishes
|
||||
* execution.
|
||||
*
|
||||
* @param frame the current execution frame.
|
||||
* @param result the result of executing the node this method was
|
||||
* triggered for.
|
||||
* @param result the result of executing the node this method was triggered for.
|
||||
*/
|
||||
@Override
|
||||
public void onReturnValue(VirtualFrame frame, Object result) {
|
||||
@ -146,15 +227,28 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
}
|
||||
Node node = context.getInstrumentedNode();
|
||||
|
||||
if (node instanceof FunctionCallInstrumentationNode
|
||||
&& result instanceof FunctionCallInstrumentationNode.FunctionCall functionCall) {
|
||||
UUID nodeId = ((FunctionCallInstrumentationNode) node).getId();
|
||||
var cachedResult = callbacks.onFunctionReturn(nodeId, functionCall);
|
||||
if (node instanceof FunctionCallInstrumentationNode functionCallInstrumentationNode
|
||||
&& result instanceof FunctionCallInstrumentationNode.FunctionCall) {
|
||||
Info info =
|
||||
new NodeInfo(
|
||||
functionCallInstrumentationNode.getId(),
|
||||
result,
|
||||
nanoTimeElapsed,
|
||||
frame.materialize(),
|
||||
node);
|
||||
Object cachedResult = callbacks.onFunctionReturn(info);
|
||||
if (cachedResult != null) {
|
||||
throw context.createUnwind(cachedResult);
|
||||
}
|
||||
} else if (node instanceof ExpressionNode) {
|
||||
onExpressionReturn(result, node, context, nanoTimeElapsed);
|
||||
} else if (node instanceof ExpressionNode expressionNode) {
|
||||
Info info =
|
||||
new NodeInfo(
|
||||
expressionNode.getId(), result, nanoTimeElapsed, frame.materialize(), node);
|
||||
callbacks.updateCachedResult(info);
|
||||
|
||||
if (info.isPanic()) {
|
||||
throw context.createUnwind(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,25 +263,13 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
}
|
||||
}
|
||||
|
||||
private void onExpressionReturn(Object result, Node node, EventContext context, long howLong) {
|
||||
boolean isPanic = result instanceof AbstractTruffleException && !(result instanceof DataflowError);
|
||||
UUID nodeId = ((ExpressionNode) node).getId();
|
||||
|
||||
callbacks.updateCachedResult(nodeId, result, isPanic, howLong);
|
||||
if (isPanic) {
|
||||
throw context.createUnwind(result);
|
||||
}
|
||||
}
|
||||
|
||||
@CompilerDirectives.TruffleBoundary
|
||||
private void onTailCallReturn(Throwable exception, State state) {
|
||||
try {
|
||||
TailCallException tailCallException = (TailCallException) exception;
|
||||
FunctionCallInstrumentationNode.FunctionCall functionCall
|
||||
= new FunctionCallInstrumentationNode.FunctionCall(
|
||||
tailCallException.getFunction(),
|
||||
state,
|
||||
tailCallException.getArguments());
|
||||
FunctionCallInstrumentationNode.FunctionCall functionCall =
|
||||
new FunctionCallInstrumentationNode.FunctionCall(
|
||||
tailCallException.getFunction(), state, tailCallException.getArguments());
|
||||
Object result = InteropLibrary.getFactory().getUncached().execute(functionCall);
|
||||
onReturnValue(null, result);
|
||||
} catch (InteropException e) {
|
||||
@ -196,47 +278,39 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're not inside a recursive call, i.e. the
|
||||
* {@link #entryCallTarget} only appears in the stack trace once.
|
||||
* Checks if we're not inside a recursive call, i.e. the {@link #entryCallTarget} only appears
|
||||
* in the stack trace once.
|
||||
*
|
||||
* @return {@code true} if it's not a recursive call, {@code false}
|
||||
* otherwise.
|
||||
* @return {@code true} if it's not a recursive call, {@code false} otherwise.
|
||||
*/
|
||||
private boolean isTopFrame(CallTarget entryCallTarget) {
|
||||
Object result = Truffle.getRuntime().iterateFrames(new FrameInstanceVisitor<Object>() {
|
||||
boolean seenFirst = false;
|
||||
Object result =
|
||||
Truffle.getRuntime()
|
||||
.iterateFrames(
|
||||
new FrameInstanceVisitor<Object>() {
|
||||
boolean seenFirst = false;
|
||||
|
||||
@Override
|
||||
public Object visitFrame(FrameInstance frameInstance) {
|
||||
CallTarget ct = frameInstance.getCallTarget();
|
||||
if (ct != entryCallTarget) {
|
||||
return null;
|
||||
}
|
||||
if (seenFirst) {
|
||||
return new Object();
|
||||
} else {
|
||||
seenFirst = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public Object visitFrame(FrameInstance frameInstance) {
|
||||
CallTarget ct = frameInstance.getCallTarget();
|
||||
if (ct != entryCallTarget) {
|
||||
return null;
|
||||
}
|
||||
if (seenFirst) {
|
||||
return new Object();
|
||||
} else {
|
||||
seenFirst = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
return result == null;
|
||||
}
|
||||
|
||||
private static UUID getNodeId(Node node) {
|
||||
return switch (node) {
|
||||
case ExpressionNode n -> n.getId();
|
||||
case FunctionCallInstrumentationNode n -> n.getId();
|
||||
case null -> null;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a new event node factory to observe identified nodes within given
|
||||
* function.
|
||||
* Attach a new event node factory to observe identified nodes within given function.
|
||||
*
|
||||
* @param mod module that contains the code
|
||||
* @param entryCallTarget the call target being observed.
|
||||
@ -246,19 +320,19 @@ public class IdExecutionInstrument extends TruffleInstrument implements IdExecut
|
||||
*/
|
||||
@Override
|
||||
public EventBinding<ExecutionEventNodeFactory> bind(
|
||||
TruffleObject mod,
|
||||
CallTarget entryCallTarget,
|
||||
Callbacks callbacks,
|
||||
Object timer
|
||||
) {
|
||||
var module = (Module)mod;
|
||||
var builder = SourceSectionFilter.newBuilder()
|
||||
TruffleObject mod, CallTarget entryCallTarget, Callbacks callbacks, Object timer) {
|
||||
var module = (Module) mod;
|
||||
var builder =
|
||||
SourceSectionFilter.newBuilder()
|
||||
.tagIs(StandardTags.ExpressionTag.class, StandardTags.CallTag.class)
|
||||
.tagIs(IdentifiedTag.class)
|
||||
.tagIsNot(AvoidIdInstrumentationTag.class)
|
||||
.sourceIs(module::isModuleSource);
|
||||
|
||||
if (entryCallTarget instanceof RootCallTarget r && r.getRootNode() instanceof ClosureRootNode c && c.getSourceSection() instanceof SourceSection section && section != null) {
|
||||
if (entryCallTarget instanceof RootCallTarget r
|
||||
&& r.getRootNode() instanceof ClosureRootNode c
|
||||
&& c.getSourceSection() instanceof SourceSection section
|
||||
&& section != null) {
|
||||
final int firstFunctionLine = section.getStartLine();
|
||||
final int afterFunctionLine = section.getEndLine() + 1;
|
||||
builder.lineIn(SourceSectionFilter.IndexRange.between(firstFunctionLine, afterFunctionLine));
|
||||
|
@ -3513,4 +3513,365 @@ class RuntimeVisualizationsTest
|
||||
}
|
||||
new String(data1, StandardCharsets.UTF_8) shouldEqual "C"
|
||||
}
|
||||
|
||||
it should "execute expression in the scope of local expression cached" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
val metadata = new Metadata
|
||||
|
||||
val idOp1 = metadata.addItem(23, 2)
|
||||
val idOp2 = metadata.addItem(42, 13)
|
||||
|
||||
val code =
|
||||
"""main =
|
||||
| operator1 = 42
|
||||
| operator2 = operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|fun1 x = x.to_text
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Open the new file
|
||||
context.send(
|
||||
Api.Request(requestId, Api.OpenFileRequest(mainFile, contents))
|
||||
)
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(Some(requestId), Api.OpenFileResponse)
|
||||
)
|
||||
|
||||
// push main
|
||||
val item1 = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
context.receiveNIgnorePendingExpressionUpdates(
|
||||
4
|
||||
) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, idOp1, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idOp2, ConstantsGen.INTEGER_BUILTIN),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
|
||||
// execute expression
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.ExecuteExpression(
|
||||
contextId,
|
||||
visualizationId,
|
||||
idOp2,
|
||||
"fun1 operator1"
|
||||
)
|
||||
)
|
||||
)
|
||||
val executeExpressionResponses =
|
||||
context.receiveNIgnoreExpressionUpdates(3)
|
||||
executeExpressionResponses should contain allOf (
|
||||
Api.Response(requestId, Api.VisualizationAttached()),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
val Some(data) = executeExpressionResponses.collectFirst {
|
||||
case Api.Response(
|
||||
None,
|
||||
Api.VisualizationUpdate(
|
||||
Api.VisualizationContext(
|
||||
`visualizationId`,
|
||||
`contextId`,
|
||||
`idOp2`
|
||||
),
|
||||
data
|
||||
)
|
||||
) =>
|
||||
data
|
||||
}
|
||||
new String(data) shouldEqual "42"
|
||||
}
|
||||
|
||||
it should "execute expression in the scope of local expression not cached" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
val metadata = new Metadata
|
||||
|
||||
val idOp1 = metadata.addItem(23, 2)
|
||||
val idOp2 = metadata.addItem(42, 13)
|
||||
val idRes = metadata.addItem(60, 9)
|
||||
|
||||
val code =
|
||||
"""main =
|
||||
| operator1 = 42
|
||||
| operator2 = operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|fun1 x = x.to_text
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Open the new file
|
||||
context.send(
|
||||
Api.Request(requestId, Api.OpenFileRequest(mainFile, contents))
|
||||
)
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(Some(requestId), Api.OpenFileResponse)
|
||||
)
|
||||
|
||||
// push main
|
||||
val item1 = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
context.receiveNIgnorePendingExpressionUpdates(
|
||||
5
|
||||
) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, idOp1, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idOp2, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idRes, ConstantsGen.INTEGER_BUILTIN),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
|
||||
// execute expression
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.ExecuteExpression(
|
||||
contextId,
|
||||
visualizationId,
|
||||
idRes,
|
||||
"fun1 operator1"
|
||||
)
|
||||
)
|
||||
)
|
||||
val executeExpressionResponses =
|
||||
context.receiveNIgnoreExpressionUpdates(3)
|
||||
executeExpressionResponses should contain allOf (
|
||||
Api.Response(requestId, Api.VisualizationAttached()),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
val Some(data) = executeExpressionResponses.collectFirst {
|
||||
case Api.Response(
|
||||
None,
|
||||
Api.VisualizationUpdate(
|
||||
Api.VisualizationContext(
|
||||
`visualizationId`,
|
||||
`contextId`,
|
||||
`idRes`
|
||||
),
|
||||
data
|
||||
)
|
||||
) =>
|
||||
data
|
||||
}
|
||||
new String(data) shouldEqual "42"
|
||||
}
|
||||
|
||||
it should "execute expression in the scope of local binding" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
val metadata = new Metadata
|
||||
|
||||
val idOp1 = metadata.addItem(23, 2)
|
||||
val idOp2 = metadata.addItem(42, 13)
|
||||
val idOp2Binding = metadata.addItem(30, 25)
|
||||
val idRes = metadata.addItem(60, 9)
|
||||
|
||||
val code =
|
||||
"""main =
|
||||
| operator1 = 42
|
||||
| operator2 = operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|fun1 x = x.to_text
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Open the new file
|
||||
context.send(
|
||||
Api.Request(requestId, Api.OpenFileRequest(mainFile, contents))
|
||||
)
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(Some(requestId), Api.OpenFileResponse)
|
||||
)
|
||||
|
||||
// push main
|
||||
val item1 = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
context.receiveNIgnorePendingExpressionUpdates(
|
||||
6
|
||||
) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, idOp1, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idOp2, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages
|
||||
.update(contextId, idOp2Binding, ConstantsGen.NOTHING_BUILTIN),
|
||||
TestMessages.update(contextId, idRes, ConstantsGen.INTEGER_BUILTIN),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
|
||||
// execute expression
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.ExecuteExpression(
|
||||
contextId,
|
||||
visualizationId,
|
||||
idOp2Binding,
|
||||
"fun1 operator1+operator2"
|
||||
)
|
||||
)
|
||||
)
|
||||
val executeExpressionResponses =
|
||||
context.receiveNIgnoreExpressionUpdates(3)
|
||||
executeExpressionResponses should contain allOf (
|
||||
Api.Response(requestId, Api.VisualizationAttached()),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
val Some(data) = executeExpressionResponses.collectFirst {
|
||||
case Api.Response(
|
||||
None,
|
||||
Api.VisualizationUpdate(
|
||||
Api.VisualizationContext(
|
||||
`visualizationId`,
|
||||
`contextId`,
|
||||
`idOp2Binding`
|
||||
),
|
||||
data
|
||||
)
|
||||
) =>
|
||||
data
|
||||
}
|
||||
new String(data) shouldEqual "85"
|
||||
}
|
||||
|
||||
it should "execute expression in the scope of main method" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val visualizationId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
val metadata = new Metadata
|
||||
|
||||
val idOp1 = metadata.addItem(23, 2)
|
||||
val idOp2 = metadata.addItem(42, 13)
|
||||
val idMain = metadata.addItem(6, 63)
|
||||
|
||||
val code =
|
||||
"""main =
|
||||
| operator1 = 42
|
||||
| operator2 = operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|fun1 x = x.to_text
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Open the new file
|
||||
context.send(
|
||||
Api.Request(requestId, Api.OpenFileRequest(mainFile, contents))
|
||||
)
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(Some(requestId), Api.OpenFileResponse)
|
||||
)
|
||||
|
||||
// push main
|
||||
val item1 = Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
context.receiveNIgnorePendingExpressionUpdates(
|
||||
5
|
||||
) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, idOp1, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idOp2, ConstantsGen.INTEGER_BUILTIN),
|
||||
TestMessages.update(contextId, idMain, ConstantsGen.INTEGER_BUILTIN),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
|
||||
// execute expression
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.ExecuteExpression(
|
||||
contextId,
|
||||
visualizationId,
|
||||
idMain,
|
||||
"fun1 operator1+operator2"
|
||||
)
|
||||
)
|
||||
)
|
||||
val executeExpressionResponses =
|
||||
context.receiveNIgnoreExpressionUpdates(3)
|
||||
executeExpressionResponses should contain allOf (
|
||||
Api.Response(requestId, Api.VisualizationAttached()),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
val Some(data) = executeExpressionResponses.collectFirst {
|
||||
case Api.Response(
|
||||
None,
|
||||
Api.VisualizationUpdate(
|
||||
Api.VisualizationContext(
|
||||
`visualizationId`,
|
||||
`contextId`,
|
||||
`idMain`
|
||||
),
|
||||
data
|
||||
)
|
||||
) =>
|
||||
data
|
||||
}
|
||||
new String(data) shouldEqual "85"
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ public final class EnsoLanguage extends TruffleLanguage<EnsoContext> {
|
||||
|
||||
/**
|
||||
* Parses the given Enso source code snippet in {@code request}.
|
||||
*
|
||||
* <p>
|
||||
* Inline parsing does not handle the following expressions:
|
||||
* <ul>
|
||||
* <li>Assignments</li>
|
||||
|
@ -1,7 +1,5 @@
|
||||
package org.enso.interpreter.node.expression.builtin.meta;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.enso.interpreter.instrument.Timer;
|
||||
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
|
||||
import org.enso.interpreter.runtime.EnsoContext;
|
||||
@ -15,7 +13,6 @@ import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.instrumentation.EventBinding;
|
||||
import com.oracle.truffle.api.interop.InteropException;
|
||||
import com.oracle.truffle.api.interop.InteropLibrary;
|
||||
import com.oracle.truffle.api.interop.TruffleObject;
|
||||
|
||||
final class Instrumentor implements EnsoObject, IdExecutionService.Callbacks {
|
||||
|
||||
@ -61,33 +58,33 @@ final class Instrumentor implements EnsoObject, IdExecutionService.Callbacks {
|
||||
// Callbacks
|
||||
//
|
||||
@Override
|
||||
public Object findCachedResult(UUID nodeId) {
|
||||
public Object findCachedResult(IdExecutionService.Info info) {
|
||||
try {
|
||||
if (onEnter != null) {
|
||||
var ret = InteropLibrary.getUncached().execute(onEnter, nodeId.toString());
|
||||
var ret = InteropLibrary.getUncached().execute(onEnter, info.getId().toString());
|
||||
ret = InteropLibrary.getUncached().isNull(ret) ? null : ret;
|
||||
return handle.isDisposed() ? null : ret;
|
||||
}
|
||||
} catch (InteropException ex) {
|
||||
return handle.isDisposed() ? null : ret; }
|
||||
} catch (InteropException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCachedResult(UUID nodeId, Object result, boolean isPanic, long nanoElapsedTime) {
|
||||
public void updateCachedResult(IdExecutionService.Info info) {
|
||||
try {
|
||||
if (onReturn != null) {
|
||||
InteropLibrary.getUncached().execute(onReturn, nodeId.toString(), result);
|
||||
InteropLibrary.getUncached().execute(onReturn, info.getId().toString(), info.getResult());
|
||||
}
|
||||
} catch (InteropException ex) {
|
||||
} catch (InteropException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onFunctionReturn(UUID nodeId, TruffleObject result) {
|
||||
public Object onFunctionReturn(IdExecutionService.Info info) {
|
||||
try {
|
||||
if (onCall != null && result instanceof FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
var args = (Object[])call.getArguments().clone();
|
||||
if (onCall != null
|
||||
&& info.getResult() instanceof FunctionCallInstrumentationNode.FunctionCall call) {
|
||||
var args = (Object[]) call.getArguments().clone();
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (args[i] == null) {
|
||||
args[i] = EnsoContext.get(null).getBuiltins().nothing();
|
||||
@ -95,14 +92,14 @@ final class Instrumentor implements EnsoObject, IdExecutionService.Callbacks {
|
||||
}
|
||||
var ret = InteropLibrary.getUncached().execute(
|
||||
onCall,
|
||||
nodeId.toString(),
|
||||
info.getId().toString(),
|
||||
call.getFunction(),
|
||||
ArrayLikeHelpers.asVectorWithCheckAt(args)
|
||||
);
|
||||
ret = InteropLibrary.getUncached().isNull(ret) ? null : ret;
|
||||
return handle.isDisposed() ? null : ret;
|
||||
}
|
||||
} catch (InteropException ex) {
|
||||
} catch (InteropException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -28,9 +28,11 @@ public final class ConsoleAppender extends Appender {
|
||||
@Override
|
||||
public boolean setupForPath(
|
||||
Level logLevel, Path logRoot, String logPrefix, LoggerSetup loggerSetup) {
|
||||
if (loggerSetup.getConfig().logToFile().enabled()) {
|
||||
loggerSetup.setupFileAppender(
|
||||
loggerSetup.getConfig().logToFile().logLevel(), logRoot, logPrefix);
|
||||
LogToFile logToFileOpt = loggerSetup.getConfig().logToFile();
|
||||
if (logToFileOpt.enabled()) {
|
||||
Level minLevel =
|
||||
Level.intToLevel(Math.min(logToFileOpt.logLevel().toInt(), logLevel.toInt()));
|
||||
loggerSetup.setupFileAppender(minLevel, logRoot, logPrefix);
|
||||
}
|
||||
return loggerSetup.setupConsoleAppender(logLevel);
|
||||
}
|
||||
|
@ -115,14 +115,14 @@ public final class LogbackSetup extends LoggerSetup {
|
||||
int port) {
|
||||
Level targetLogLevel;
|
||||
// Modify log level if we were asked to always log to a file.
|
||||
// The receiver needs to get all logs (up to `trace`) so as to be able to log all verbose messages.
|
||||
// The receiver needs to get all logs (up to `trace`) to be able to log all verbose messages.
|
||||
if (logToFileEnabled()) {
|
||||
int min = Math.min(Level.TRACE.toInt(), config.logToFile().logLevel().toInt());
|
||||
int min = Math.min(logLevel.toInt(), config.logToFile().logLevel().toInt());
|
||||
targetLogLevel = Level.intToLevel(min);
|
||||
} else {
|
||||
targetLogLevel = logLevel;
|
||||
}
|
||||
LoggerAndContext env = contextInit(targetLogLevel, config, !logToFileEnabled());
|
||||
LoggerAndContext env = contextInit(targetLogLevel, config, true);
|
||||
|
||||
org.enso.logger.config.SocketAppender appenderConfig = config.getSocketAppender();
|
||||
|
||||
|
@ -26,7 +26,17 @@ public class LoggingServiceManager {
|
||||
throw new LoggingServiceAlreadySetup();
|
||||
} else {
|
||||
if (config.appenders().containsKey(config.appender())) {
|
||||
currentLevel = config.logToFile().enabled() ? config.logToFile().logLevel() : logLevel;
|
||||
if (config.logToFile().enabled()) {
|
||||
String envSetLogLevel = System.getenv("ENSO_LOG_TO_FILE_LOG_LEVEL");
|
||||
if (envSetLogLevel != null) {
|
||||
currentLevel = config.logToFile().logLevel();
|
||||
} else {
|
||||
int min = Math.min(config.logToFile().logLevel().toInt(), logLevel.toInt());
|
||||
currentLevel = Level.intToLevel(min);
|
||||
}
|
||||
} else {
|
||||
currentLevel = logLevel;
|
||||
}
|
||||
return Future.apply(
|
||||
() -> {
|
||||
var server = LoggingServiceFactory.get().localServerFor(port);
|
||||
|
@ -64,7 +64,8 @@ logging-service {
|
||||
max-file-size = "100MB"
|
||||
max-history = 30
|
||||
max-total-size = "2GB"
|
||||
}
|
||||
},
|
||||
immediate-flush = true
|
||||
},
|
||||
{
|
||||
name = "sentry"
|
||||
|
Loading…
Reference in New Issue
Block a user