Merge branch 'develop' into wip/gmt/8110-map-error

This commit is contained in:
Gregory Travis 2023-11-14 09:21:34 -05:00
commit f8f0b2f597
65 changed files with 1830 additions and 671 deletions

View File

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

View File

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

View File

@ -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))

View File

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

View 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>

View File

@ -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())

View 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[]
}

View File

@ -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),

View File

@ -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.`)
}
}
)
}
})
}
})
}

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

View File

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

View 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')

View File

@ -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))

View File

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

View File

@ -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()

View File

@ -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(

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[]) => {

View 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

View File

@ -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)

View File

@ -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
}
// =====================================

View File

@ -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`

View File

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

View File

@ -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)

View File

@ -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 {

View File

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

View File

@ -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] =

View File

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

View File

@ -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()

View File

@ -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,

View File

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

View File

@ -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);
}
/**

View File

@ -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`.
*

View File

@ -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),

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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
});
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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.

View File

@ -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 {

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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.

View File

@ -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.
*

View File

@ -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));

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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();

View File

@ -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);

View File

@ -64,7 +64,8 @@ logging-service {
max-file-size = "100MB"
max-history = 30
max-total-size = "2GB"
}
},
immediate-flush = true
},
{
name = "sentry"