From 10bb0ae08370326a5de13c543cb15be8c7ff20be Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Wed, 8 Jan 2025 15:33:25 +0100 Subject: [PATCH 1/9] Vue entrypoint (#11960) Fixes #11185 Fixes #11923 I moved (and simplified) some setup. Mostly, the entry point sets up general behavior on page load, App.vue is the root, and the react-specific setups are now in `ReactRoot` component. The old App.vue is now just the ProjectView.vue. Removed some options which are no longer relevant since there's just a single GUI package. I tried to put the ProjectView component into dashboard layout, but unfortunately, veaury is not helpful here, as it unsets all styles in its wrapper. Also, GUI no longer displays Help view; if there's an unrecognized option we just print a warning and continue. --- app/gui/.storybook/preview.tsx | 2 +- app/gui/index.html | 2 +- .../dashboard/createAsset.spec.ts | 9 +- .../dashboard/driveView.spec.ts | 9 +- .../dashboard/pageSwitcher.spec.ts | 2 +- .../dashboard/startModal.spec.ts | 2 +- .../graphNodeVisualization.spec.ts | 5 +- app/gui/src/App.vue | 78 ++++++ app/gui/src/ReactRoot.tsx | 76 ++++++ app/gui/src/dashboard/App.tsx | 17 +- .../AriaComponents/Dialog/utilities.ts | 2 +- app/gui/src/dashboard/index.tsx | 168 ------------- app/gui/src/dashboard/layouts/Editor.tsx | 107 +++----- .../dashboard/pages/dashboard/Dashboard.tsx | 5 +- .../pages/dashboard/DashboardTabPanels.tsx | 5 +- app/gui/src/dashboard/styles.css | 4 +- app/gui/src/dashboard/tailwind.css | 4 +- app/gui/src/entrypoint.ts | 234 +++++++++++------- app/gui/src/project-view/App.vue | 180 -------------- app/gui/src/project-view/ProjectView.vue | 135 ++++++++++ app/gui/src/project-view/ProjectViewTab.vue | 23 ++ app/gui/src/project-view/asyncApp.ts | 7 - .../project-view/components/GraphEditor.vue | 7 +- app/gui/src/project-view/config.json | 15 -- .../project-view/providers/eventLogging.ts | 7 +- .../src/project-view/stores/project/index.ts | 82 +++--- app/gui/src/project-view/test-entrypoint.ts | 35 +-- app/gui/src/project-view/views/AboutView.vue | 15 -- .../src/project-view/views/ProjectView.vue | 16 -- app/gui/vite.config.ts | 2 - package.json | 2 +- 31 files changed, 572 insertions(+), 685 deletions(-) create mode 100644 app/gui/src/App.vue create mode 100644 app/gui/src/ReactRoot.tsx delete mode 100644 app/gui/src/dashboard/index.tsx delete mode 100644 app/gui/src/project-view/App.vue create mode 100644 app/gui/src/project-view/ProjectView.vue create mode 100644 app/gui/src/project-view/ProjectViewTab.vue delete mode 100644 app/gui/src/project-view/asyncApp.ts delete mode 100644 app/gui/src/project-view/views/AboutView.vue delete mode 100644 app/gui/src/project-view/views/ProjectView.vue diff --git a/app/gui/.storybook/preview.tsx b/app/gui/.storybook/preview.tsx index a984a548119e..fbcc5e609e85 100644 --- a/app/gui/.storybook/preview.tsx +++ b/app/gui/.storybook/preview.tsx @@ -66,7 +66,7 @@ const reactPreview: ReactPreview = { (Story, context) => ( <> -
+
diff --git a/app/gui/index.html b/app/gui/index.html index 2fd49ff74630..c6225a07d42e 100644 --- a/app/gui/index.html +++ b/app/gui/index.html @@ -50,7 +50,7 @@
-
+
diff --git a/app/gui/integration-test/dashboard/createAsset.spec.ts b/app/gui/integration-test/dashboard/createAsset.spec.ts index a9ae229e685f..79688a9e721b 100644 --- a/app/gui/integration-test/dashboard/createAsset.spec.ts +++ b/app/gui/integration-test/dashboard/createAsset.spec.ts @@ -1,5 +1,5 @@ /** @file Test copying, moving, cutting and pasting. */ -import { expect, test, type Page } from '@playwright/test' +import { expect, test } from '@playwright/test' import { mockAllAndLogin } from './actions' @@ -12,13 +12,6 @@ const SECRET_NAME = 'a secret name' /** The value of the created secret. */ const SECRET_VALUE = 'a secret value' -/** Find an editor container. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function locateEditor(page: Page) { - // Test ID of a placeholder editor component used during testing. - return page.locator('.App') -} - test('create folder', ({ page }) => mockAllAndLogin({ page }) .createFolder() diff --git a/app/gui/integration-test/dashboard/driveView.spec.ts b/app/gui/integration-test/dashboard/driveView.spec.ts index 883f54ba9425..607b039042c3 100644 --- a/app/gui/integration-test/dashboard/driveView.spec.ts +++ b/app/gui/integration-test/dashboard/driveView.spec.ts @@ -1,15 +1,8 @@ /** @file Test the drive view. */ -import { expect, test, type Locator, type Page } from '@playwright/test' +import { expect, test, type Locator } from '@playwright/test' import { TEXT, mockAllAndLogin } from './actions' -/** Find an editor container. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function locateEditor(page: Page) { - // Test ID of a placeholder editor component used during testing. - return page.locator('.App') -} - /** Find a button to close the project. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars function locateStopProjectButton(page: Locator) { diff --git a/app/gui/integration-test/dashboard/pageSwitcher.spec.ts b/app/gui/integration-test/dashboard/pageSwitcher.spec.ts index de26fe3bded7..5efac7ffbf8e 100644 --- a/app/gui/integration-test/dashboard/pageSwitcher.spec.ts +++ b/app/gui/integration-test/dashboard/pageSwitcher.spec.ts @@ -6,7 +6,7 @@ import { mockAllAndLogin } from './actions' /** Find an editor container. */ function locateEditor(page: Page) { // Test ID of a placeholder editor component used during testing. - return page.locator('.App') + return page.locator('.ProjectView') } /** Find a drive view. */ diff --git a/app/gui/integration-test/dashboard/startModal.spec.ts b/app/gui/integration-test/dashboard/startModal.spec.ts index 3c1ef1b6b8cb..26c46abda262 100644 --- a/app/gui/integration-test/dashboard/startModal.spec.ts +++ b/app/gui/integration-test/dashboard/startModal.spec.ts @@ -6,7 +6,7 @@ import { mockAllAndLogin } from './actions' /** Find an editor container. */ function locateEditor(page: Page) { // Test ID of a placeholder editor component used during testing. - return page.locator('.App') + return page.locator('.ProjectView') } /** Find a samples list. */ diff --git a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts index af27ca9159c9..6b8d9ddd0861 100644 --- a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts +++ b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts @@ -3,6 +3,7 @@ import assert from 'assert' import * as actions from './actions' import { computedContent } from './css' import { expect } from './customExpect' +import { CONTROL_KEY } from './keyboard' import * as locate from './locate' test('Node can open and load visualization', async ({ page }) => { @@ -50,10 +51,12 @@ test('Previewing visualization', async ({ page }) => { test('Warnings visualization', async ({ page }) => { await actions.goToGraph(page) - + // Without centering the graph, menu sometimes goes out of the view. + await page.keyboard.press(`${CONTROL_KEY}+Shift+A`) // Create a node, attach a warning, open the warnings-visualization. await locate.addNewNodeButton(page).click() const input = locate.componentBrowserInput(page).locator('input') + await input.fill('Warning.attach "Uh oh" 42') await page.keyboard.press('Enter') await expect(locate.componentBrowser(page)).toBeHidden() diff --git a/app/gui/src/App.vue b/app/gui/src/App.vue new file mode 100644 index 000000000000..c81288cfaf3e --- /dev/null +++ b/app/gui/src/App.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/app/gui/src/ReactRoot.tsx b/app/gui/src/ReactRoot.tsx new file mode 100644 index 000000000000..000ed9ae4372 --- /dev/null +++ b/app/gui/src/ReactRoot.tsx @@ -0,0 +1,76 @@ +/** @file A file containing setup for React part of application. */ + +import App from '#/App.tsx' +import { ReactQueryDevtools } from '#/components/Devtools' +import { ErrorBoundary } from '#/components/ErrorBoundary' +import { OfflineNotificationManager } from '#/components/OfflineNotificationManager' +import { Suspense } from '#/components/Suspense' +import UIProviders from '#/components/UIProviders' +import LoadingScreen from '#/pages/authentication/LoadingScreen' +import { HttpClientProvider } from '#/providers/HttpClientProvider' +import LoggerProvider from '#/providers/LoggerProvider' +import HttpClient from '#/utilities/HttpClient' +import { ApplicationConfigValue } from '@/util/config' +import { QueryClientProvider } from '@tanstack/react-query' +import { QueryClient } from '@tanstack/vue-query' +import { IS_DEV_MODE, isOnElectron, isOnLinux } from 'enso-common/src/detect' +import { StrictMode } from 'react' +import invariant from 'tiny-invariant' + +interface ReactRootProps { + config: ApplicationConfigValue + queryClient: QueryClient + classSet: Map + onAuthenticated: (accessToken: string | null) => void +} + +function resolveEnvUrl(url: string | undefined) { + return url?.replace('__HOSTNAME__', window.location.hostname) +} + +/** + * A component gathering all views written currently in React with necessary contexts. + */ +export default function ReactRoot(props: ReactRootProps) { + const { config, queryClient, onAuthenticated } = props + + const httpClient = new HttpClient() + const supportsDeepLinks = !IS_DEV_MODE && !isOnLinux() && isOnElectron() + const portalRoot = document.querySelector('#enso-portal-root') + const shouldUseAuthentication = config.authentication.enabled + const projectManagerUrl = + (config.engine.projectManagerUrl || resolveEnvUrl(PROJECT_MANAGER_URL)) ?? null + const ydocUrl = (config.engine.ydocUrl || resolveEnvUrl(YDOC_SERVER_URL)) ?? null + const initialProjectName = config.startup.project || null + invariant(portalRoot, 'PortalRoot element not found') + + return ( + + + + + }> + + + + + + + + + + + + + + + ) +} diff --git a/app/gui/src/dashboard/App.tsx b/app/gui/src/dashboard/App.tsx index 6d04f6c9c6b6..5580ac6e2eb2 100644 --- a/app/gui/src/dashboard/App.tsx +++ b/app/gui/src/dashboard/App.tsx @@ -70,7 +70,6 @@ import Dashboard from '#/pages/dashboard/Dashboard' import * as subscribe from '#/pages/subscribe/Subscribe' import * as subscribeSuccess from '#/pages/subscribe/SubscribeSuccess' -import type * as editor from '#/layouts/Editor' import * as openAppWatcher from '#/layouts/OpenAppWatcher' import VersionChecker from '#/layouts/VersionChecker' @@ -143,7 +142,6 @@ function getMainPageUrl() { /** Global configuration for the `App` component. */ export interface AppProps { - readonly vibrancy: boolean /** Whether the application may have the local backend running. */ readonly supportsLocalBackend: boolean /** If true, the app can only be used in offline mode. */ @@ -153,15 +151,11 @@ export interface AppProps { * the installed app on macOS and Windows. */ readonly supportsDeepLinks: boolean - /** Whether the dashboard should be rendered. */ - readonly shouldShowDashboard: boolean /** The name of the project to open on startup, if any. */ readonly initialProjectName: string | null readonly onAuthenticated: (accessToken: string | null) => void readonly projectManagerUrl: string | null readonly ydocUrl: string | null - readonly appRunner: editor.GraphEditorRunner | null - readonly queryClient: reactQuery.QueryClient } /** @@ -217,8 +211,7 @@ export default function App(props: AppProps) { const { isOffline } = useOffline() const { getText } = textProvider.useText() - - const queryClient = props.queryClient + const queryClient = reactQuery.useQueryClient() // Force all queries to be stale // We don't use the `staleTime` option because it's not performant @@ -304,8 +297,7 @@ export interface AppRouterProps extends AppProps { * component as the component that defines the provider. */ function AppRouter(props: AppRouterProps) { - const { isAuthenticationDisabled, shouldShowDashboard } = props - const { onAuthenticated, projectManagerInstance } = props + const { isAuthenticationDisabled, onAuthenticated, projectManagerInstance } = props const httpClient = useHttpClientStrict() const logger = useLogger() const navigate = router.useNavigate() @@ -483,10 +475,7 @@ function AppRouter(props: AppRouterProps) { }> }> }> - } - /> + } /> { - if ( - (event.breadcrumbs ?? []).some( - (breadcrumb) => - breadcrumb.type === 'http' && - breadcrumb.category === 'fetch' && - breadcrumb.data && - breadcrumb.data.status_code === HTTP_STATUS_BAD_REQUEST && - typeof breadcrumb.data.url === 'string' && - new URL(breadcrumb.data.url).host === API_HOST, - ) - ) { - return null - } - return event - }, - }) - } - - if (vibrancy) { - document.body.classList.add('vibrancy') - } - - const root = document.getElementById(ROOT_ELEMENT_ID) - const portalRoot = document.querySelector('#enso-portal-root') - - invariant(root, 'Root element not found') - invariant(portalRoot, 'PortalRoot element not found') - - // `supportsDeepLinks` will be incorrect when accessing the installed Electron app's pages - // via the browser. - const actuallySupportsDeepLinks = - detect.IS_DEV_MODE ? supportsDeepLinks : supportsDeepLinks && detect.isOnElectron() - - const httpClient = new HttpClient() - - startTransition(() => { - reactDOM.createRoot(root).render( - - - - - }> - - - - - - - - - - - - - - , - ) - }) -} - -/** Global configuration for the {@link App} component. */ -export type AppProps = app.AppProps diff --git a/app/gui/src/dashboard/layouts/Editor.tsx b/app/gui/src/dashboard/layouts/Editor.tsx index 073a28951a89..f8eb85580168 100644 --- a/app/gui/src/dashboard/layouts/Editor.tsx +++ b/app/gui/src/dashboard/layouts/Editor.tsx @@ -1,67 +1,34 @@ /** @file The container that launches the IDE. */ -import * as React from 'react' - -import * as reactQuery from '@tanstack/react-query' - -import * as appUtils from '#/appUtils' - +import * as errorBoundary from '#/components/ErrorBoundary' +import * as suspense from '#/components/Suspense' +import { useEventCallback } from '#/hooks/eventCallbackHooks' import * as gtagHooks from '#/hooks/gtagHooks' import * as projectHooks from '#/hooks/projectHooks' - import * as backendProvider from '#/providers/BackendProvider' import type { LaunchedProject } from '#/providers/ProjectsProvider' import * as textProvider from '#/providers/TextProvider' - -import * as errorBoundary from '#/components/ErrorBoundary' -import * as suspense from '#/components/Suspense' - -import type Backend from '#/services/Backend' import * as backendModule from '#/services/Backend' - -import { useEventCallback } from '#/hooks/eventCallbackHooks' import * as twMerge from '#/utilities/tailwindMerge' +import * as reactQuery from '@tanstack/react-query' +import * as React from 'react' import { useTimeoutCallback } from '../hooks/timeoutHooks' - -// ==================== -// === StringConfig === -// ==================== - -/** A configuration in which values may be strings or nested configurations. */ -interface StringConfig { - readonly [key: string]: StringConfig | string -} - -// ======================== -// === GraphEditorProps === -// ======================== +// eslint-disable-next-line no-restricted-syntax +import ProjectViewTabVue from '@/ProjectViewTab.vue' +import { applyPureVueInReact } from 'veaury' +import type { AllowedComponentProps, VNodeProps } from 'vue' +import type { ComponentProps } from 'vue-component-type-helpers' /** Props for the GUI editor root component. */ -export interface GraphEditorProps { - readonly config: StringConfig | null - readonly projectId: string - readonly hidden: boolean - readonly ignoreParamsRegex?: RegExp - readonly logEvent: (message: string, projectId?: string | null, metadata?: object | null) => void - readonly renameProject: (newName: string) => void - readonly projectBackend: Backend | null - readonly remoteBackend: Backend | null -} - -// ========================= -// === GraphEditorRunner === -// ========================= +export type ProjectViewTabProps = Omit< + ComponentProps, + keyof AllowedComponentProps | keyof VNodeProps +> -/** - * The value passed from the entrypoint to the dashboard, which enables the dashboard to - * open a new IDE instance. - */ -export type GraphEditorRunner = React.ComponentType - -// ================= -// === Constants === -// ================= - -const IGNORE_PARAMS_REGEX = new RegExp(`^${appUtils.SEARCH_PARAMS_PREFIX}(.+)$`) +// applyPureVuewInReact returns Function, but this is not enough to satisfy TSX. +// eslint-disable-next-line no-restricted-syntax +const ProjectViewTab = applyPureVueInReact(ProjectViewTabVue) as ( + props: ProjectViewTabProps, +) => JSX.Element // ============== // === Editor === @@ -75,7 +42,6 @@ export interface EditorProps { readonly project: LaunchedProject readonly hidden: boolean readonly ydocUrl: string | null - readonly appRunner: GraphEditorRunner | null readonly renameProject: (newName: string, projectId: backendModule.ProjectId) => void readonly projectId: backendModule.ProjectId } @@ -187,7 +153,7 @@ interface EditorInternalProps extends Omit { /** An internal editor. */ function EditorInternal(props: EditorInternalProps) { - const { hidden, ydocUrl, appRunner: AppRunner, renameProject, openedProject, backendType } = props + const { hidden, ydocUrl, renameProject, openedProject, backendType } = props const { getText } = textProvider.useText() const gtagEvent = gtagHooks.useGtagEvent() @@ -195,13 +161,6 @@ function EditorInternal(props: EditorInternalProps) { const localBackend = backendProvider.useLocalBackend() const remoteBackend = backendProvider.useRemoteBackend() - const logEvent = React.useCallback( - (message: string, projectId?: string | null, metadata?: object | null) => { - void remoteBackend.logEvent(message, projectId, metadata) - }, - [remoteBackend], - ) - React.useEffect(() => { if (!hidden) { return gtagHooks.gtagOpenCloseCallback(gtagEvent, 'open_workflow', 'close_workflow') @@ -212,7 +171,7 @@ function EditorInternal(props: EditorInternalProps) { renameProject(newName, openedProject.projectId) }) - const appProps = React.useMemo(() => { + const appProps = React.useMemo(() => { const jsonAddress = openedProject.jsonAddress const binaryAddress = openedProject.binaryAddress const ydocAddress = openedProject.ydocAddress ?? ydocUrl ?? '' @@ -225,18 +184,16 @@ function EditorInternal(props: EditorInternalProps) { throw new Error(getText('noBinaryEndpointError')) } else { return { - config: { + hidden, + projectViewProps: { + projectId: openedProject.projectId, + projectName: openedProject.packageName, + projectDisplayedName: openedProject.name, engine: { rpcUrl: jsonAddress, dataUrl: binaryAddress, ydocUrl: ydocAddress }, - startup: { project: openedProject.packageName, displayedProjectName: openedProject.name }, - window: { topBarOffset: '0' }, + renameProject: onRenameProject, + projectBackend, + remoteBackend, }, - projectId: openedProject.projectId, - hidden, - ignoreParamsRegex: IGNORE_PARAMS_REGEX, - logEvent, - renameProject: onRenameProject, - projectBackend, - remoteBackend, } } }, [ @@ -244,16 +201,18 @@ function EditorInternal(props: EditorInternalProps) { ydocUrl, getText, hidden, - logEvent, onRenameProject, backendType, localBackend, remoteBackend, ]) + // EsLint does not handle types imported from vue files and their dependences. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const key: string = appProps.projectViewProps.projectId // Currently the GUI component needs to be fully rerendered whenever the project is changed. Once // this is no longer necessary, the `key` could be removed. - return AppRunner == null ? null : + return } export default React.memo(Editor) diff --git a/app/gui/src/dashboard/pages/dashboard/Dashboard.tsx b/app/gui/src/dashboard/pages/dashboard/Dashboard.tsx index 0600894f0131..fa415b3875d1 100644 --- a/app/gui/src/dashboard/pages/dashboard/Dashboard.tsx +++ b/app/gui/src/dashboard/pages/dashboard/Dashboard.tsx @@ -33,7 +33,6 @@ import type * as assetTable from '#/layouts/AssetsTable' import Chat from '#/layouts/Chat' import ChatPlaceholder from '#/layouts/ChatPlaceholder' import EventListProvider, * as eventListProvider from '#/layouts/Drive/EventListProvider' -import type * as editor from '#/layouts/Editor' import UserBar from '#/layouts/UserBar' import * as aria from '#/components/aria' @@ -61,7 +60,6 @@ import { DashboardTabPanels } from './DashboardTabPanels' export interface DashboardProps { /** Whether the application may have the local backend running. */ readonly supportsLocalBackend: boolean - readonly appRunner: editor.GraphEditorRunner | null readonly initialProjectName: string | null readonly ydocUrl: string | null } @@ -106,7 +104,7 @@ function fileURLToPath(url: string): string | null { /** The component that contains the entire UI. */ function DashboardInner(props: DashboardProps) { - const { appRunner, initialProjectName: initialProjectNameRaw, ydocUrl } = props + const { initialProjectName: initialProjectNameRaw, ydocUrl } = props const { user } = authProvider.useFullUserSession() const localBackend = backendProvider.useLocalBackend() const { modalRef } = modalProvider.useModalRef() @@ -282,7 +280,6 @@ function DashboardInner(props: DashboardProps) {
| null @@ -25,7 +23,7 @@ export interface DashboardTabPanelsProps { /** The tab panels for the dashboard page. */ export function DashboardTabPanels(props: DashboardTabPanelsProps) { - const { appRunner, initialProjectName, ydocUrl, assetManagementApiRef } = props + const { initialProjectName, ydocUrl, assetManagementApiRef } = props const page = usePage() @@ -68,7 +66,6 @@ export function DashboardTabPanels(props: DashboardTabPanelsProps) { ydocUrl={ydocUrl} project={project} projectId={project.id} - appRunner={appRunner} isOpeningFailed={openProjectMutation.isError} openingError={openProjectMutation.error} startProject={openProjectMutation.mutate} diff --git a/app/gui/src/dashboard/styles.css b/app/gui/src/dashboard/styles.css index d670163c2092..af36dfdd18dc 100644 --- a/app/gui/src/dashboard/styles.css +++ b/app/gui/src/dashboard/styles.css @@ -20,6 +20,6 @@ } } -:where(.enso-dashboard) { - @apply absolute inset-0 isolate flex flex-col overflow-hidden; +:where(.enso-app) { + @apply absolute inset-0 isolate overflow-hidden; } diff --git a/app/gui/src/dashboard/tailwind.css b/app/gui/src/dashboard/tailwind.css index b38559898897..5cdb2af6081e 100644 --- a/app/gui/src/dashboard/tailwind.css +++ b/app/gui/src/dashboard/tailwind.css @@ -405,8 +405,8 @@ } /* These styles MUST still be copied - * as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */ -:where(.enso-dashboard, .enso-chat, .enso-portal-root) { + * as `.enso-app body` and `.enso-app html` make no sense. */ +:where(.enso-app, .enso-chat, .enso-portal-root) { line-height: 1.5; -webkit-text-size-adjust: 100%; -moz-tab-size: 4; diff --git a/app/gui/src/entrypoint.ts b/app/gui/src/entrypoint.ts index 5d275f20b430..50f81dcbf313 100644 --- a/app/gui/src/entrypoint.ts +++ b/app/gui/src/entrypoint.ts @@ -1,57 +1,154 @@ -import * as dashboard from '#/index' import '#/styles.css' import '#/tailwind.css' -import { AsyncApp } from '@/asyncApp' -import { baseConfig, configValue, mergeConfig } from '@/util/config' -import { urlParams } from '@/util/urlParams' -import * as vueQuery from '@tanstack/vue-query' -import { isOnLinux } from 'enso-common/src/detect' -import * as commonQuery from 'enso-common/src/queryClient' +import * as sentry from '@sentry/react' +import { VueQueryPlugin } from '@tanstack/vue-query' +import * as detect from 'enso-common/src/detect' +import { createQueryClient } from 'enso-common/src/queryClient' +import { MotionGlobalConfig } from 'framer-motion' import * as idbKeyval from 'idb-keyval' -import { lazyVueInReact } from 'veaury' -import { type App } from 'vue' +import { useEffect } from 'react' +import { + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom' +import { createApp } from 'vue' +import App from './App.vue' -const INITIAL_URL_KEY = `Enso-initial-url` +const HTTP_STATUS_BAD_REQUEST = 400 +const API_HOST = + process.env.ENSO_CLOUD_API_URL != null ? new URL(process.env.ENSO_CLOUD_API_URL).host : null +/** The fraction of non-erroring interactions that should be sampled by Sentry. */ +const SENTRY_SAMPLE_RATE = 0.005 const SCAM_WARNING_TIMEOUT = 1000 -export const isDevMode = process.env.NODE_ENV === 'development' - -function printScamWarning() { - if (isDevMode) return - const headerCss = ` - color: white; - background: crimson; - display: block; - border-radius: 8px; - font-weight: bold; - padding: 10px 20px 10px 20px; - ` - .trim() - .replace(/\n\s+/, ' ') - const headerCss1 = headerCss + ' font-size: 46px;' - const headerCss2 = headerCss + ' font-size: 20px;' - const msgCSS = 'font-size: 16px;' - - const msg1 = - 'This is a browser feature intended for developers. If someone told you to ' + - 'copy-paste something here, it is a scam and will give them access to your ' + - 'account and data.' - const msg2 = 'See https://enso.org/selfxss for more information.' - console.log('%cStop!', headerCss1) - console.log('%cYou may be the victim of a scam!', headerCss2) - console.log('%c' + msg1, msgCSS) - console.log('%c' + msg2, msgCSS) +const INITIAL_URL_KEY = `Enso-initial-url` + +function main() { + setupScamWarning() + setupSentry() + configureAnimations() + const appProps = imNotSureButPerhapsFixingRefreshingWithAuthentication() + const queryClient = createQueryClientOfPersistCache() + + const app = createApp(App, appProps) + app.use(VueQueryPlugin, { queryClient }) + app.mount('#enso-app') } -printScamWarning() -let scamWarningHandle = 0 +function setupScamWarning() { + function printScamWarning() { + if (process.env.NODE_ENV === 'development') return + const headerCss = ` + color: white; + background: crimson; + display: block; + border-radius: 8px; + font-weight: bold; + padding: 10px 20px 10px 20px; + ` + .trim() + .replace(/\n\s+/, ' ') + const headerCss1 = headerCss + ' font-size: 46px;' + const headerCss2 = headerCss + ' font-size: 20px;' + const msgCSS = 'font-size: 16px;' + + const msg1 = + 'This is a browser feature intended for developers. If someone told you to ' + + 'copy-paste something here, it is a scam and will give them access to your ' + + 'account and data.' + const msg2 = 'See https://enso.org/selfxss for more information.' + console.log('%cStop!', headerCss1) + console.log('%cYou may be the victim of a scam!', headerCss2) + console.log('%c' + msg1, msgCSS) + console.log('%c' + msg2, msgCSS) + } -window.addEventListener('resize', () => { - window.clearTimeout(scamWarningHandle) - scamWarningHandle = window.setTimeout(printScamWarning, SCAM_WARNING_TIMEOUT) -}) + printScamWarning() + let scamWarningHandle = 0 -/** The entrypoint into the IDE. */ -function main() { + window.addEventListener('resize', () => { + window.clearTimeout(scamWarningHandle) + scamWarningHandle = window.setTimeout(printScamWarning, SCAM_WARNING_TIMEOUT) + }) +} + +function setupSentry() { + if ( + !detect.IS_DEV_MODE && + process.env.ENSO_CLOUD_SENTRY_DSN != null && + process.env.ENSO_CLOUD_API_URL != null + ) { + const version: unknown = import.meta.env.ENSO_IDE_VERSION + sentry.init({ + dsn: process.env.ENSO_CLOUD_SENTRY_DSN, + environment: process.env.ENSO_CLOUD_ENVIRONMENT, + release: version?.toString() ?? 'dev', + integrations: [ + sentry.reactRouterV6BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + sentry.extraErrorDataIntegration({ captureErrorCause: true }), + sentry.replayIntegration(), + new sentry.BrowserProfilingIntegration(), + ], + profilesSampleRate: SENTRY_SAMPLE_RATE, + tracesSampleRate: SENTRY_SAMPLE_RATE, + tracePropagationTargets: [process.env.ENSO_CLOUD_API_URL.split('//')[1] ?? ''], + replaysSessionSampleRate: SENTRY_SAMPLE_RATE, + replaysOnErrorSampleRate: 1.0, + beforeSend: (event) => { + if ( + (event.breadcrumbs ?? []).some( + (breadcrumb) => + breadcrumb.type === 'http' && + breadcrumb.category === 'fetch' && + breadcrumb.data && + breadcrumb.data.status_code === HTTP_STATUS_BAD_REQUEST && + typeof breadcrumb.data.url === 'string' && + new URL(breadcrumb.data.url).host === API_HOST, + ) + ) { + return null + } + return event + }, + }) + } +} + +function configureAnimations() { + const areAnimationsDisabled = + window.DISABLE_ANIMATIONS === true || + localStorage.getItem('disableAnimations') === 'true' || + false + + MotionGlobalConfig.skipAnimations = areAnimationsDisabled + + if (areAnimationsDisabled) { + document.documentElement.classList.add('disable-animations') + } else { + document.documentElement.classList.remove('disable-animations') + } +} + +function createQueryClientOfPersistCache() { + const store = idbKeyval.createStore('enso', 'query-persist-cache') + return createQueryClient({ + persisterStorage: { + getItem: async (key) => idbKeyval.get(key, store), + setItem: async (key, value) => idbKeyval.set(key, value, store), + removeItem: async (key) => idbKeyval.del(key, store), + clear: async () => idbKeyval.clear(store), + }, + }) +} + +function imNotSureButPerhapsFixingRefreshingWithAuthentication() { /** * Note: Signing out always redirects to `/`. It is impossible to make this work, * as it is not possible to distinguish between having just logged out, and explicitly @@ -73,49 +170,7 @@ function main() { localStorage.setItem(INITIAL_URL_KEY, location.href) } - const resolveEnvUrl = (url: string | undefined) => - url?.replace('__HOSTNAME__', window.location.hostname) - - const config = configValue(mergeConfig(baseConfig, urlParams())) - const supportsVibrancy = config.window.vibrancy - const shouldUseAuthentication = config.authentication.enabled - const projectManagerUrl = - (config.engine.projectManagerUrl || resolveEnvUrl(PROJECT_MANAGER_URL)) ?? null - const ydocUrl = (config.engine.ydocUrl || resolveEnvUrl(YDOC_SERVER_URL)) ?? null - const initialProjectName = config.startup.project || null - const urlWithoutStartupProject = new URL(location.toString()) - urlWithoutStartupProject.searchParams.delete('startup.project') - history.replaceState(null, '', urlWithoutStartupProject) - - const store = idbKeyval.createStore('enso', 'query-persist-cache') - const queryClient = commonQuery.createQueryClient({ - persisterStorage: { - getItem: async (key) => idbKeyval.get(key, store), - setItem: async (key, value) => idbKeyval.set(key, value, store), - removeItem: async (key) => idbKeyval.del(key, store), - clear: async () => idbKeyval.clear(store), - }, - }) - - const registerPlugins = (app: App) => { - app.use(vueQuery.VueQueryPlugin, { queryClient }) - } - - const appRunner = lazyVueInReact(AsyncApp as any /* async VueComponent */, { - beforeVueAppMount: (app) => registerPlugins(app as App), - }) as dashboard.GraphEditorRunner - - dashboard.run({ - appRunner, - logger: console, - vibrancy: supportsVibrancy, - supportsLocalBackend: !IS_CLOUD_BUILD, - supportsDeepLinks: !isDevMode && !isOnLinux(), - projectManagerUrl, - ydocUrl, - isAuthenticationDisabled: !shouldUseAuthentication, - shouldShowDashboard: true, - initialProjectName, + return { onAuthenticated() { if (isInAuthenticationFlow) { const initialUrl = localStorage.getItem(INITIAL_URL_KEY) @@ -126,8 +181,7 @@ function main() { } } }, - queryClient, - }) + } } main() diff --git a/app/gui/src/project-view/App.vue b/app/gui/src/project-view/App.vue deleted file mode 100644 index 0127b8110599..000000000000 --- a/app/gui/src/project-view/App.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - - - diff --git a/app/gui/src/project-view/ProjectView.vue b/app/gui/src/project-view/ProjectView.vue new file mode 100644 index 000000000000..d9f818b13f16 --- /dev/null +++ b/app/gui/src/project-view/ProjectView.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/app/gui/src/project-view/ProjectViewTab.vue b/app/gui/src/project-view/ProjectViewTab.vue new file mode 100644 index 000000000000..438831003cbd --- /dev/null +++ b/app/gui/src/project-view/ProjectViewTab.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/gui/src/project-view/asyncApp.ts b/app/gui/src/project-view/asyncApp.ts deleted file mode 100644 index bf60c6c69187..000000000000 --- a/app/gui/src/project-view/asyncApp.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '@/assets/base.css' - -/** Load App.vue asynchronously. */ -export async function AsyncApp() { - const app = await import('@/App.vue') - return app -} diff --git a/app/gui/src/project-view/components/GraphEditor.vue b/app/gui/src/project-view/components/GraphEditor.vue index 6b42c5843f09..c474ccf60d14 100644 --- a/app/gui/src/project-view/components/GraphEditor.vue +++ b/app/gui/src/project-view/components/GraphEditor.vue @@ -690,11 +690,8 @@ const groupColors = computed(() => { diff --git a/app/gui/src/project-view/views/ProjectView.vue b/app/gui/src/project-view/views/ProjectView.vue deleted file mode 100644 index 3c20d457072f..000000000000 --- a/app/gui/src/project-view/views/ProjectView.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/app/gui/vite.config.ts b/app/gui/vite.config.ts index decff30fc04a..017ae978b669 100644 --- a/app/gui/vite.config.ts +++ b/app/gui/vite.config.ts @@ -1,6 +1,4 @@ import { sentryVitePlugin } from '@sentry/vite-plugin' -/// - import react from '@vitejs/plugin-react' import vue from '@vitejs/plugin-vue' import { COOP_COEP_CORP_HEADERS } from 'enso-common' diff --git a/package.json b/package.json index 469a858029a5..9a2cfd5424ce 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "ci:chromatic:react": "corepack pnpm run -r --filter enso-gui chromatic:react", "ci:chromatic:vue": "corepack pnpm run -r --filter enso-gui chromatic:vue" }, + "//": "To completely ignore deep dependencies, see .pnpmfile.cjs", "pnpm": { - "//": "To completely ignore deep dependencies, see .pnpmfile.cjs", "overrides": { "tslib": "$tslib", "jsdom": "^24.1.0", From 787372e86e47c9aced2fadfc65906650fb21272e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Wa=C5=9Bko?= Date: Wed, 8 Jan 2025 16:40:59 +0100 Subject: [PATCH 2/9] Infer method calls on known types and warn about nonexistent methods (#11399) - Implements #9812 - Allows us to propagate type inference through method calls, at least in a basic way. - Adds a 'lint warning': when calling a method on an object of a known specific (non-`Any`) type that does not exist on that type, a warning is reported, because such a call would always result in `No_Such_Method` error at runtime. - `Any` has special behaviour - if `x : Any` we don't know what `x` might be, so we allow all methods on it and defer to the runtime to see if they will be valid or not. - This check is not proving correctness, but instead it's only triggering for 'provably wrong' situations. - Includes changes from #11955: - removing obsolete `AtomTypeInterface`, - simplifying `TypeRepresentation` to be a plain data structure, - and removing a cycle from IR in `BindingsMap` in favour of relying on `StaticModuleScope`. --- .../src/Internal/Ordering_Helpers.enso | 2 + .../Base/0.0.0-dev/src/Meta/Enso_Project.enso | 3 + .../Base/0.0.0-dev/src/System/File.enso | 6 + .../org/enso/common/CompilationStage.java | 13 +- .../src/main/java/module-info.java | 1 + .../enso/compiler/MetadataInteropHelpers.java | 16 +- .../common/BuildScopeFromModuleAlgorithm.java | 180 ++++ .../common/MethodResolutionAlgorithm.java | 222 +++++ .../NameResolutionAlgorithm.java | 8 +- .../pass/analyse/PassPersistance.java | 14 +- .../pass/analyse/types/AtomTypeInterface.java | 34 - .../AtomTypeInterfaceFromBindingsMap.java | 77 -- .../pass/analyse/types/BuiltinTypes.java | 45 +- .../pass/analyse/types/CommonTypeHelpers.java | 17 - .../pass/analyse/types/InferredType.java | 4 +- .../analyse/types/MethodTypeResolver.java | 34 + .../pass/analyse/types/TypeCompatibility.java | 10 +- .../types/TypeInferencePersistance.java | 1 - ...nce.java => TypeInferencePropagation.java} | 108 +- .../types/TypeInferenceSignatures.java | 196 ++++ .../pass/analyse/types/TypePropagation.java | 206 +++- .../analyse/types/TypeRepresentation.java | 40 +- .../pass/analyse/types/TypeResolver.java | 113 +-- .../types/scope/AtomTypeDefinition.java | 43 + .../types/scope/BuiltinsFallbackScope.java | 37 + .../analyse/types/scope/ModuleResolver.java | 37 + .../types/scope/StaticImportExportScope.java | 92 ++ .../types/scope/StaticMethodResolution.java | 118 +++ .../types/scope/StaticModuleScope.java | 174 ++++ .../scope/StaticModuleScopeAnalysis.java | 245 +++++ .../analyse/types/scope/TypeHierarchy.java | 42 + .../types/scope/TypeScopeReference.java | 96 ++ .../scala/org/enso/compiler/Compiler.scala | 59 +- .../main/scala/org/enso/compiler/Passes.scala | 20 +- .../enso/compiler/context/InlineContext.scala | 13 +- .../org/enso/compiler/data/BindingsMap.scala | 27 +- .../org/enso/compiler/pass/PassManager.scala | 49 - .../compiler/test/CompilerTestSetup.scala | 2 +- .../enso/compiler/test/SerdeCompilerTest.java | 2 +- .../test/SerializationManagerTest.java | 2 +- .../compiler/test/StaticAnalysisTest.java | 117 +++ .../enso/compiler/test/TypeInferenceTest.java | 927 +++++++++++++----- .../test/TypesFromSignaturesTest.java | 188 ++++ .../enso/interpreter/test/SignatureTest.java | 46 + .../test/TypeInferenceConsistencyTest.java | 2 +- .../org/enso/compiler/test/CompilerTest.scala | 2 +- .../pass/analyse/BindingAnalysisTest.scala | 10 +- .../enso/compiler/core/ir/IrPersistance.java | 2 + .../org/enso/compiler/core/ir/Warning.scala | 18 + .../runtime/scope/ImportExportScope.java | 12 +- .../runtime/scope/ModuleScope.java | 137 ++- .../runtime/scope/TopLevelScope.java | 8 +- .../interpreter/runtime/IrToTruffle.scala | 472 ++++----- .../test/TypeMetadataPersistanceTest.java | 22 +- .../main/java/org/enso/persist/PerMap.java | 3 +- .../java/org/enso/test/utils/ModuleUtils.java | 71 ++ .../src/main/scala/org/enso/pkg/Config.scala | 3 - .../lib/Standard/Base/0.0.0-dev/src/Any.enso | 3 + .../lib/Standard/Base/0.0.0-dev/src/Meta.enso | 2 + 59 files changed, 3442 insertions(+), 1011 deletions(-) create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/common/BuildScopeFromModuleAlgorithm.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/common/MethodResolutionAlgorithm.java rename engine/runtime-compiler/src/main/java/org/enso/compiler/{context => common}/NameResolutionAlgorithm.java (92%) delete mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterface.java delete mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterfaceFromBindingsMap.java delete mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/CommonTypeHelpers.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/MethodTypeResolver.java rename engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/{TypeInference.java => TypeInferencePropagation.java} (61%) create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferenceSignatures.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/AtomTypeDefinition.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/BuiltinsFallbackScope.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/ModuleResolver.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticImportExportScope.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticMethodResolution.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScope.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScopeAnalysis.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeHierarchy.java create mode 100644 engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeScopeReference.java create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/StaticAnalysisTest.java create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypesFromSignaturesTest.java diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Ordering_Helpers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Ordering_Helpers.enso index 4cb1b8cd3222..fdc7df7195b2 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Ordering_Helpers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Ordering_Helpers.enso @@ -37,8 +37,10 @@ type Default_Comparator ## PRIVATE hash : Number -> Integer hash x = Default_Comparator.hash_builtin x + ## PRIVATE hash_builtin x = @Builtin_Method "Default_Comparator.hash_builtin" + ## PRIVATE less_than_builtin left right = @Builtin_Method "Default_Comparator.less_than_builtin" diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso index feccbb2e7476..cc7ecfd01db9 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso @@ -96,6 +96,9 @@ type Project_Description root_path : Text root_path self = self.root.path + ## PRIVATE + enso_project_builtin module = @Builtin_Method "Project_Description.enso_project_builtin" + ## ICON enso_icon Returns the Enso project description for the project that the engine was executed with, i.e., the project that contains the `main` method, or diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso index 51decf6624fe..ca2dd010b691 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso @@ -835,6 +835,12 @@ type File to_display_text : Text to_display_text self = self.to_text + ## PRIVATE + copy_builtin self target options = @Builtin_Method "File.copy_builtin" + + ## PRIVATE + move_builtin self target options = @Builtin_Method "File.move_builtin" + ## PRIVATE Utility function that returns all descendants of the provided file, including diff --git a/engine/common/src/main/java/org/enso/common/CompilationStage.java b/engine/common/src/main/java/org/enso/common/CompilationStage.java index cf6a85083a20..8da418f39052 100644 --- a/engine/common/src/main/java/org/enso/common/CompilationStage.java +++ b/engine/common/src/main/java/org/enso/common/CompilationStage.java @@ -3,12 +3,13 @@ /** Defines a stage of compilation of the module. */ public enum CompilationStage { INITIAL(0), - AFTER_PARSING(1), - AFTER_IMPORT_RESOLUTION(2), - AFTER_GLOBAL_TYPES(3), - AFTER_STATIC_PASSES(4), - AFTER_RUNTIME_STUBS(5), - AFTER_CODEGEN(6); + AFTER_PARSING(10), + AFTER_IMPORT_RESOLUTION(20), + AFTER_GLOBAL_TYPES(30), + AFTER_STATIC_PASSES(40), + AFTER_TYPE_INFERENCE_PASSES(45), + AFTER_RUNTIME_STUBS(50), + AFTER_CODEGEN(60); private final int ordinal; diff --git a/engine/runtime-compiler/src/main/java/module-info.java b/engine/runtime-compiler/src/main/java/module-info.java index e081f014144a..e834881f7825 100644 --- a/engine/runtime-compiler/src/main/java/module-info.java +++ b/engine/runtime-compiler/src/main/java/module-info.java @@ -30,4 +30,5 @@ exports org.enso.compiler.phase; exports org.enso.compiler.phase.exports; exports org.enso.compiler.refactoring; + exports org.enso.compiler.common; } diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/MetadataInteropHelpers.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/MetadataInteropHelpers.java index 4b7f270df95d..e32134ec53e7 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/MetadataInteropHelpers.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/MetadataInteropHelpers.java @@ -34,7 +34,21 @@ public static T getMetadataOrNull(IR ir, IRProcessingPass pass, Class exp public static T getMetadata(IR ir, IRProcessingPass pass, Class expectedType) { T metadataOrNull = getMetadataOrNull(ir, pass, expectedType); if (metadataOrNull == null) { - throw new IllegalStateException("Missing expected " + pass + " metadata for " + ir + "."); + String textRepresentation = ir.toString(); + if (textRepresentation.length() > 100) { + textRepresentation = textRepresentation.substring(0, 100) + "..."; + } + + throw new IllegalStateException( + "Missing expected " + + pass + + " metadata for " + + textRepresentation + + " (" + + ir.getClass().getCanonicalName() + + "), had " + + ir.passData().toString() + + "."); } return metadataOrNull; diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/common/BuildScopeFromModuleAlgorithm.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/BuildScopeFromModuleAlgorithm.java new file mode 100644 index 000000000000..082c1534cba6 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/BuildScopeFromModuleAlgorithm.java @@ -0,0 +1,180 @@ +package org.enso.compiler.common; + +import org.enso.compiler.MetadataInteropHelpers; +import org.enso.compiler.core.ir.Module; +import org.enso.compiler.core.ir.module.scope.Definition; +import org.enso.compiler.core.ir.module.scope.definition.Method; +import org.enso.compiler.core.ir.module.scope.imports.Polyglot; +import org.enso.compiler.data.BindingsMap; +import org.enso.compiler.pass.resolve.MethodDefinitions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.jdk.javaapi.CollectionConverters; + +/** + * Gathers the common logic for building the ModuleScope. + * + *

This is done in two places: + * + *

    + *
  1. in the compiler, gathering just the types to build StaticModuleScope, + *
  2. in the runtime, building Truffle nodes for the interpreter. + *
+ * + *

The interpreter does much more than the type-checker, so currently this only gathers the + * general shape of the process to try to ensure that they stay in sync. In future iterations, we + * may try to move more of the logic to this common place. + */ +public abstract class BuildScopeFromModuleAlgorithm { + private final Logger logger = LoggerFactory.getLogger(BuildScopeFromModuleAlgorithm.class); + + protected abstract void registerExport(ImportExportScopeType exportScope); + + protected abstract void registerImport(ImportExportScopeType importScope); + + protected abstract TypeScopeReferenceType getTypeAssociatedWithCurrentScope(); + + /** Runs the main processing on a module, that will build the module scope for it. */ + public final void processModule(Module moduleIr, BindingsMap bindingsMap) { + processModuleExports(bindingsMap); + processModuleImports(bindingsMap); + processPolyglotImports(moduleIr); + + processBindings(moduleIr); + } + + private void processModuleExports(BindingsMap bindingsMap) { + for (var exportedMod : + CollectionConverters.asJavaCollection(bindingsMap.getDirectlyExportedModules())) { + ImportExportScopeType exportScope = buildExportScope(exportedMod); + registerExport(exportScope); + } + } + + private void processModuleImports(BindingsMap bindingsMap) { + for (var imp : CollectionConverters.asJavaCollection(bindingsMap.resolvedImports())) { + for (var target : CollectionConverters.asJavaCollection(imp.targets())) { + if (target instanceof BindingsMap.ResolvedModule resolvedModule) { + var importScope = buildImportScope(imp, resolvedModule); + registerImport(importScope); + } + } + } + } + + private void processPolyglotImports(Module moduleIr) { + for (var imp : CollectionConverters.asJavaCollection(moduleIr.imports())) { + if (imp instanceof Polyglot polyglotImport) { + if (polyglotImport.entity() instanceof Polyglot.Java javaEntity) { + processPolyglotJavaImport(polyglotImport.getVisibleName(), javaEntity.getJavaName()); + } else { + throw new IllegalStateException( + "Unsupported polyglot import entity: " + polyglotImport.entity()); + } + } + } + } + + private void processBindings(Module module) { + for (var binding : CollectionConverters.asJavaCollection(module.bindings())) { + switch (binding) { + case Definition.Type typ -> processTypeDefinition(typ); + case Method.Explicit method -> processMethodDefinition(method); + case Method.Conversion conversion -> processConversion(conversion); + default -> logger.warn( + "Unexpected binding type: {}", binding.getClass().getCanonicalName()); + } + } + } + + /** Allows the implementation to specify how to register polyglot Java imports. */ + protected abstract void processPolyglotJavaImport(String visibleName, String javaClassName); + + /** + * Allows the implementation to specify how to register conversions. + * + *

In the future we may want to extract some common logic from this, but for now we allow the + * implementation to specify this. + */ + protected abstract void processConversion(Method.Conversion conversion); + + /** Allows the implementation to specify how to register method definitions. */ + protected abstract void processMethodDefinition(Method.Explicit method); + + /** + * Allows the implementation to specify how to register type definitions, along with their + * constructors and getters. + * + *

The type registration (registering constructors, getters) is really complex, ideally we'd + * also like to extract some common logic from it. But the differences are very large, so setting + * that aside for later. + */ + protected abstract void processTypeDefinition(Definition.Type typ); + + /** + * Common method that allows to extract the type on which the method is defined. + * + *

    + *
  • For a member method, this will be its parent type. + *
  • For a static method, this will be the eigentype of the type on which it is defined. + *
  • For a module method, this will be the type associated with the module. + *
+ */ + protected final TypeScopeReferenceType getTypeDefiningMethod(Method.Explicit method) { + var typePointerOpt = method.methodReference().typePointer(); + if (typePointerOpt.isEmpty()) { + return getTypeAssociatedWithCurrentScope(); + } else { + var metadata = + MetadataInteropHelpers.getMetadataOrNull( + typePointerOpt.get(), MethodDefinitions.INSTANCE, BindingsMap.Resolution.class); + if (metadata == null) { + logger.debug( + "Failed to resolve type pointer for method: {}", method.methodReference().showCode()); + return null; + } + + return switch (metadata.target()) { + case BindingsMap.ResolvedType resolvedType -> associatedTypeFromResolvedType( + resolvedType, method.isStatic()); + case BindingsMap.ResolvedModule resolvedModule -> associatedTypeFromResolvedModule( + resolvedModule); + default -> throw new IllegalStateException( + "Unexpected target type: " + metadata.target().getClass().getCanonicalName()); + }; + } + } + + /** + * Implementation specific piece of {@link #getTypeDefiningMethod(Method.Explicit)} that specifies + * how to build the associated type from a resolved module. + */ + protected abstract TypeScopeReferenceType associatedTypeFromResolvedModule( + BindingsMap.ResolvedModule module); + + /** + * Implementation specific piece of {@link #getTypeDefiningMethod(Method.Explicit)} that specifies + * how to build the associated type from a resolved type, depending on if the method is static or + * not. + */ + protected abstract TypeScopeReferenceType associatedTypeFromResolvedType( + BindingsMap.ResolvedType type, boolean isStatic); + + /** + * Allows the implementation to specify how to build the export scope from an exported module + * instance. + * + *

Such scope is then registered with the scope builder using {@code addExport}. + */ + protected abstract ImportExportScopeType buildExportScope( + BindingsMap.ExportedModule exportedModule); + + /** + * Allows the implementation to specify how to build the import scope from a resolved import and + * module. + * + *

Such scope is then registered with the scope builder using {@code addImport}. + */ + protected abstract ImportExportScopeType buildImportScope( + BindingsMap.ResolvedImport resolvedImport, BindingsMap.ResolvedModule resolvedModule); +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/common/MethodResolutionAlgorithm.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/MethodResolutionAlgorithm.java new file mode 100644 index 000000000000..ef6d1dca861d --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/MethodResolutionAlgorithm.java @@ -0,0 +1,222 @@ +package org.enso.compiler.common; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Encapsulates the logic for resolving a method call on a type/module. + * + *

The same logic is needed in two places: + * + *

    + *
  1. in the runtime ({@link + * org.enso.interpreter.runtime.scope.ModuleScope#lookupMethodDefinition}), + *
  2. in the type checker ({@link org.enso.compiler.pass.analyse.types.MethodTypeResolver}). + *
+ * + *

To ensure that all usages stay consistent, they should all rely on the logic implemented in + * this class, customizing it to the specific needs of the context in which it is used. + * + * @param the type of the module scope that the algorithm will be working with + */ +public abstract class MethodResolutionAlgorithm< + FunctionType, TypeScopeReferenceType, ImportExportScopeType, ModuleScopeType> { + + /** + * Looks up a method definition as seen in the current module. + * + *

This takes into consideration all definitions local to the module and everything that has + * been imported. + * + *

The algorithm is as follows: + * + *

    + *
  1. Methods defined in the same module as the type which is being called have the highest + * precedence. + *
  2. Next, methods defined in the current module are considered. + *
  3. Finally, methods imported from other modules. + *
+ */ + public final FunctionType lookupMethodDefinition( + ModuleScopeType currentModuleScope, TypeScopeReferenceType type, String methodName) { + var definitionScope = findDefinitionScope(type); + if (definitionScope != null) { + var definedWithAtom = getMethodFromModuleScope(definitionScope, type, methodName); + if (definedWithAtom != null) { + return definedWithAtom; + } + } + + var definedHere = getMethodFromModuleScope(currentModuleScope, type, methodName); + if (definedHere != null) { + return definedHere; + } + + return findInImports(currentModuleScope, type, methodName); + } + + /** + * Finds a method exported by a module. + * + *

It first checks methods defined in the module and later checks any methods re-exported from + * other modules. + */ + public final FunctionType getExportedMethod( + ModuleScopeType moduleScope, TypeScopeReferenceType type, String methodName) { + var definedLocally = getMethodFromModuleScope(moduleScope, type, methodName); + if (definedLocally != null) { + return definedLocally; + } + + return getExportsFromModuleScope(moduleScope).stream() + .map(scope -> getMethodForTypeFromScope(scope, type, methodName)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * Looks up a conversion definition as seen in the current module. + * + *

The algorithm is as follows: + * + *

    + *
  1. Conversions defined in the definition module of the source type are looked-up first, + *
  2. Next, conversions defined in the definition module of the target type are considered, + *
  3. Then, conversions defined in the current module are considered, + *
  4. Finally, conversions imported from other modules are considered. + *
+ */ + public final FunctionType lookupConversionDefinition( + ModuleScopeType currentModuleScope, + TypeScopeReferenceType source, + TypeScopeReferenceType target) { + var sourceDefinitionScope = findDefinitionScope(source); + var definedWithSource = getConversionFromModuleScope(sourceDefinitionScope, target, source); + if (definedWithSource != null) { + return definedWithSource; + } + + var targetDefinitionScope = findDefinitionScope(target); + var definedWithTarget = getConversionFromModuleScope(targetDefinitionScope, target, source); + if (definedWithTarget != null) { + return definedWithTarget; + } + + var definedHere = getConversionFromModuleScope(currentModuleScope, target, source); + if (definedHere != null) { + return definedHere; + } + + return getImportsFromModuleScope(currentModuleScope).stream() + .map(scope -> getExportedConversionFromScope(scope, target, source)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * Finds a conversion exported by a module. + * + *

It first checks conversions defined in the module and later checks any conversions + * re-exported from other modules. + */ + public final FunctionType getExportedConversion( + ModuleScopeType moduleScope, TypeScopeReferenceType target, TypeScopeReferenceType source) { + var definedLocally = getConversionFromModuleScope(moduleScope, target, source); + if (definedLocally != null) { + return definedLocally; + } + + return getExportsFromModuleScope(moduleScope).stream() + .map(scope -> getConversionFromScope(scope, target, source)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private FunctionType findInImports( + ModuleScopeType currentModuleScope, TypeScopeReferenceType type, String methodName) { + var found = + getImportsFromModuleScope(currentModuleScope).stream() + .flatMap( + (importExportScope) -> { + var exportedMethod = + getExportedMethodFromScope(importExportScope, type, methodName); + if (exportedMethod != null) { + return Stream.of(new MethodFromImport<>(exportedMethod, importExportScope)); + } else { + return Stream.empty(); + } + }) + .toList(); + + if (found.size() == 1) { + return found.get(0).resolutionResult; + } else if (found.size() > 1) { + return onMultipleDefinitionsFromImports(methodName, found); + } else { + return null; + } + } + + protected abstract Collection getImportsFromModuleScope( + ModuleScopeType moduleScope); + + protected abstract Collection getExportsFromModuleScope( + ModuleScopeType moduleScope); + + protected abstract FunctionType getConversionFromModuleScope( + ModuleScopeType moduleScope, TypeScopeReferenceType target, TypeScopeReferenceType source); + + protected abstract FunctionType getMethodFromModuleScope( + ModuleScopeType moduleScope, TypeScopeReferenceType type, String methodName); + + /** Locates the module scope in which the provided type was defined. */ + protected abstract ModuleScopeType findDefinitionScope(TypeScopeReferenceType type); + + /** + * Implementation detail that should delegate to a {@code getMethodReference} variant in the given + * scope. + */ + protected abstract FunctionType getMethodForTypeFromScope( + ImportExportScopeType scope, TypeScopeReferenceType type, String methodName); + + /** + * Implementation detail that should delegate to a {@code getExportedMethod} variant in the given + * scope. + */ + protected abstract FunctionType getExportedMethodFromScope( + ImportExportScopeType scope, TypeScopeReferenceType type, String methodName); + + /** + * Implementation detail that should delegate to a {@code getConversionForType} variant in the + * given scope. + */ + protected abstract FunctionType getConversionFromScope( + ImportExportScopeType scope, TypeScopeReferenceType target, TypeScopeReferenceType source); + + /** + * Implementation detail that should delegate to a {@code getExportedConversion} variant in the + * given scope. + */ + protected abstract FunctionType getExportedConversionFromScope( + ImportExportScopeType scope, TypeScopeReferenceType target, TypeScopeReferenceType source); + + /** + * Defines the behaviour when a method resolving to distinct results is found in multiple imports. + */ + protected abstract FunctionType onMultipleDefinitionsFromImports( + String methodName, List> imports); + + /** + * Represents a method found in an import scope. + * + * @param resolutionResult the result of the resolution + * @param origin the scope in which it was found + */ + protected record MethodFromImport( + FunctionType resolutionResult, ImportExportScopeType origin) {} +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/context/NameResolutionAlgorithm.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/NameResolutionAlgorithm.java similarity index 92% rename from engine/runtime-compiler/src/main/java/org/enso/compiler/context/NameResolutionAlgorithm.java rename to engine/runtime-compiler/src/main/java/org/enso/compiler/common/NameResolutionAlgorithm.java index dc62bdd49758..c1d73d81f7df 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/context/NameResolutionAlgorithm.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/common/NameResolutionAlgorithm.java @@ -1,7 +1,8 @@ -package org.enso.compiler.context; +package org.enso.compiler.common; import org.enso.compiler.MetadataInteropHelpers; import org.enso.compiler.core.ConstantsNames; +import org.enso.compiler.core.IR; import org.enso.compiler.core.ir.Name.Literal; import org.enso.compiler.data.BindingsMap; import org.enso.compiler.pass.resolve.GlobalNames$; @@ -50,7 +51,7 @@ public final ResultType resolveName(Literal name, MetadataType meta) { name, GlobalNames$.MODULE$, BindingsMap.Resolution.class); if (global != null) { BindingsMap.ResolvedName resolution = global.target(); - return resolveGlobalName(resolution); + return resolveGlobalName(resolution, name); } if (name.name().equals(ConstantsNames.FROM_MEMBER)) { @@ -70,7 +71,8 @@ public final ResultType resolveName(Literal name, MetadataType meta) { protected abstract ResultType resolveLocalName(LocalNameLinkType localLink); - protected abstract ResultType resolveGlobalName(BindingsMap.ResolvedName resolvedName); + protected abstract ResultType resolveGlobalName( + BindingsMap.ResolvedName resolvedName, IR relatedIr); protected abstract ResultType resolveFromConversion(); diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/PassPersistance.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/PassPersistance.java index 7de99f29b66e..3a63743b5f87 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/PassPersistance.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/PassPersistance.java @@ -5,7 +5,9 @@ import org.enso.compiler.pass.analyse.alias.AliasMetadata; import org.enso.compiler.pass.analyse.alias.graph.Graph; import org.enso.compiler.pass.analyse.alias.graph.GraphOccurrence; -import org.enso.compiler.pass.analyse.types.TypeInference; +import org.enso.compiler.pass.analyse.types.TypeInferencePropagation; +import org.enso.compiler.pass.analyse.types.TypeInferenceSignatures; +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScopeAnalysis; import org.enso.compiler.pass.resolve.DocumentationComments; import org.enso.compiler.pass.resolve.DocumentationComments$; import org.enso.compiler.pass.resolve.ExpressionAnnotations$; @@ -62,10 +64,12 @@ @Persistable(clazz = AliasMetadata.RootScope.class, id = 1262, allowInlining = false) @Persistable(clazz = AliasMetadata.ChildScope.class, id = 1263, allowInlining = false) @Persistable(clazz = Graph.Link.class, id = 1266, allowInlining = false) -@Persistable(clazz = TypeInference.class, id = 1280) -@Persistable(clazz = FramePointerAnalysis$.class, id = 1281) -@Persistable(clazz = TailCall.TailPosition.class, id = 1282) -@Persistable(clazz = CachePreferences.class, id = 1284) +@Persistable(clazz = TypeInferencePropagation.class, id = 1280) +@Persistable(clazz = TypeInferenceSignatures.class, id = 1281) +@Persistable(clazz = FramePointerAnalysis$.class, id = 1282) +@Persistable(clazz = TailCall.TailPosition.class, id = 1284) +@Persistable(clazz = CachePreferences.class, id = 1285) +@Persistable(clazz = StaticModuleScopeAnalysis.class, id = 1287) public final class PassPersistance { private PassPersistance() {} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterface.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterface.java deleted file mode 100644 index 8253650278c2..000000000000 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterface.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.enso.compiler.pass.analyse.types; - -import java.util.List; - -/** - * Describes the visible interface of an Atom Type. - * - *

This interface declares what methods can be called on instances of that type (or statically) - * and what constructors may be called on it to create new instances. - */ -interface AtomTypeInterface { - List constructors(); - - interface Constructor { - String name(); - - List arguments(); - } - - interface Argument { - String name(); - - boolean hasDefaultValue(); - - /** - * The ascribed type of the argument. - * - *

It may be {@code null} if the type is not known. - */ - TypeRepresentation getType(TypeResolver resolver); - } - - // TODO in next iteration: static and member methods -} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterfaceFromBindingsMap.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterfaceFromBindingsMap.java deleted file mode 100644 index dce35aface59..000000000000 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/AtomTypeInterfaceFromBindingsMap.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.enso.compiler.pass.analyse.types; - -import java.util.List; -import org.enso.compiler.core.ir.Expression; -import org.enso.compiler.data.BindingsMap; -import org.enso.compiler.pass.analyse.types.util.ProxyList; -import scala.jdk.javaapi.CollectionConverters$; - -/** Implementation of {@link AtomTypeInterface} that is built from a {@link BindingsMap.Type}. */ -public final class AtomTypeInterfaceFromBindingsMap implements AtomTypeInterface { - private final BindingsMap.Type type; - - public AtomTypeInterfaceFromBindingsMap(BindingsMap.Type type) { - this.type = type; - } - - // Needed for Persistable - public BindingsMap.Type type() { - return type; - } - - @Override - public List constructors() { - return new ProxyList<>( - CollectionConverters$.MODULE$.asJava(type.members()), ConstructorFromBindingsMap::new); - } - - static class ConstructorFromBindingsMap implements Constructor { - private final BindingsMap.Cons constructor; - - ConstructorFromBindingsMap(BindingsMap.Cons constructor) { - this.constructor = constructor; - } - - @Override - public String name() { - return constructor.name(); - } - - private transient List arguments = null; - - @Override - public List arguments() { - return new ProxyList<>( - CollectionConverters$.MODULE$.asJava(constructor.arguments()), - ArgumentFromBindingsMap::new); - } - } - - private static class ArgumentFromBindingsMap implements Argument { - private final BindingsMap.Argument arg; - - public ArgumentFromBindingsMap(BindingsMap.Argument arg) { - this.arg = arg; - } - - @Override - public String name() { - return arg.name(); - } - - @Override - public boolean hasDefaultValue() { - return arg.hasDefaultValue(); - } - - @Override - public TypeRepresentation getType(TypeResolver resolver) { - if (arg.typ().isEmpty()) { - return null; - } else { - Expression expression = arg.typ().get(); - return resolver.resolveTypeExpression(expression); - } - } - } -} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/BuiltinTypes.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/BuiltinTypes.java index fe3e209fcd6e..81bcb8e68c74 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/BuiltinTypes.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/BuiltinTypes.java @@ -4,31 +4,46 @@ import org.enso.pkg.QualifiedName$; /** A helper class providing the builtin types. */ -public class BuiltinTypes { - // TODO in next iterations we will want to resolve descriptions of these types based on the loaded - // std-lib (from PackageRepository, if available). Note that if the std-lib is not imported, - // some builtin types have different names - this should be handled here in some sane way. +public final class BuiltinTypes { + private BuiltinTypes() {} - public final TypeRepresentation INTEGER = fromQualifiedName("Standard.Base.Data.Numbers.Integer"); - public final TypeRepresentation FLOAT = fromQualifiedName("Standard.Base.Data.Numbers.Float"); + public static final String FQN_NUMBER = "Standard.Base.Data.Numbers.Number"; + public static final TypeRepresentation NUMBER = fromQualifiedName(FQN_NUMBER); + static final String FQN_ANY = "Standard.Base.Any.Any"; + public static final TypeRepresentation TEXT = fromQualifiedName("Standard.Base.Data.Text.Text"); + public static final TypeRepresentation BOOLEAN = + fromQualifiedName("Standard.Base.Data.Boolean.Boolean"); + public static final TypeRepresentation VECTOR = + fromQualifiedName("Standard.Base.Data.Vector.Vector"); + public static final TypeRepresentation NOTHING = + fromQualifiedName("Standard.Base.Nothing.Nothing"); - public final TypeRepresentation NUMBER = fromQualifiedName("Standard.Base.Data.Numbers.Number"); - public final TypeRepresentation TEXT = fromQualifiedName("Standard.Base.Data.Text.Text"); - public final TypeRepresentation VECTOR = fromQualifiedName("Standard.Base.Data.Vector.Vector"); - public final TypeRepresentation NOTHING = fromQualifiedName("Standard.Base.Nothing.Nothing"); - - private TypeRepresentation fromQualifiedName(String qualifiedName) { + private static TypeRepresentation fromQualifiedName(String qualifiedName) { var fqn = QualifiedName$.MODULE$.fromString(qualifiedName); - return new TypeRepresentation.AtomType(fqn, null); + return new TypeRepresentation.AtomType(fqn); } + static final String FQN_FUNCTION = "Standard.Base.Function.Function"; + private static final String FQN_INTEGER = "Standard.Base.Data.Numbers.Integer"; + public static final TypeRepresentation INTEGER = fromQualifiedName(FQN_INTEGER); + private static final String FQN_FLOAT = "Standard.Base.Data.Numbers.Float"; + public static final TypeRepresentation FLOAT = fromQualifiedName(FQN_FLOAT); + public static boolean isAny(QualifiedName qualifiedName) { var str = qualifiedName.toString(); - return str.equals("Standard.Base.Any.Any") || str.equals("Standard.Base.Any"); + return str.equals(FQN_ANY); } public static boolean isFunction(QualifiedName qualifiedName) { var str = qualifiedName.toString(); - return str.equals("Standard.Base.Function.Function"); + return str.equals(FQN_FUNCTION); + } + + public static boolean isInteger(QualifiedName qualifiedName) { + return qualifiedName.toString().equals(FQN_INTEGER); + } + + public static boolean isFloat(QualifiedName qualifiedName) { + return qualifiedName.toString().equals(FQN_FLOAT); } } diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/CommonTypeHelpers.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/CommonTypeHelpers.java deleted file mode 100644 index f7ae57e6eae4..000000000000 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/CommonTypeHelpers.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.enso.compiler.pass.analyse.types; - -import org.enso.compiler.core.ir.Expression; -import org.enso.compiler.core.ir.ProcessingPass; -import scala.Option; - -class CommonTypeHelpers { - static TypeRepresentation getInferredType(Expression expression) { - Option r = expression.passData().get(TypeInference.INSTANCE); - if (r.isDefined()) { - InferredType metadata = (InferredType) r.get(); - return metadata.type(); - } else { - return null; - } - } -} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/InferredType.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/InferredType.java index 1a577e4a7cdb..d58f68c7cb3c 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/InferredType.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/InferredType.java @@ -5,10 +5,10 @@ import scala.Option; /** - * The metadata information associated with the {@link TypeInference} pass. + * The metadata information associated with the {@link TypeInferencePropagation} pass. * * @param type the type inferred for a given expression - * @see TypeInference + * @see TypeInferencePropagation */ public record InferredType(TypeRepresentation type) implements ProcessingPass.Metadata { @Override diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/MethodTypeResolver.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/MethodTypeResolver.java new file mode 100644 index 000000000000..6d862cc60adf --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/MethodTypeResolver.java @@ -0,0 +1,34 @@ +package org.enso.compiler.pass.analyse.types; + +import org.enso.compiler.pass.analyse.types.scope.ModuleResolver; +import org.enso.compiler.pass.analyse.types.scope.StaticMethodResolution; +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScope; +import org.enso.compiler.pass.analyse.types.scope.TypeHierarchy; +import org.enso.compiler.pass.analyse.types.scope.TypeScopeReference; + +/** A helper that deals with resolving types of method calls. */ +class MethodTypeResolver { + private final StaticModuleScope currentModuleScope; + private final StaticMethodResolution methodResolutionAlgorithm; + + MethodTypeResolver(ModuleResolver moduleResolver, StaticModuleScope currentModuleScope) { + this.currentModuleScope = currentModuleScope; + this.methodResolutionAlgorithm = new StaticMethodResolution(moduleResolver); + } + + TypeRepresentation resolveMethod(TypeScopeReference type, String methodName) { + var definition = + methodResolutionAlgorithm.lookupMethodDefinition(currentModuleScope, type, methodName); + if (definition != null) { + return definition; + } + + // If not found in current scope, try parents + var parent = TypeHierarchy.getParent(type); + if (parent == null) { + return null; + } + + return resolveMethod(parent, methodName); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeCompatibility.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeCompatibility.java index f581df86f287..d1cdc79adbf6 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeCompatibility.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeCompatibility.java @@ -2,9 +2,7 @@ /** A class that helps with computing compatibility between types. */ class TypeCompatibility { - TypeCompatibility(BuiltinTypes builtinTypes) { - this.builtinTypes = builtinTypes; - } + TypeCompatibility() {} /** Denotes if a given provided type can fit into an expected type. */ enum Compatibility { @@ -31,8 +29,6 @@ enum Compatibility { UNKNOWN; } - private final BuiltinTypes builtinTypes; - Compatibility computeTypeCompatibility(TypeRepresentation expected, TypeRepresentation provided) { // Exact type match is always OK. if (expected.equals(provided)) { @@ -50,8 +46,8 @@ Compatibility computeTypeCompatibility(TypeRepresentation expected, TypeRepresen return Compatibility.UNKNOWN; } - if (expected.equals(builtinTypes.NUMBER)) { - if (provided.equals(builtinTypes.INTEGER) || provided.equals(builtinTypes.FLOAT)) { + if (expected.equals(BuiltinTypes.NUMBER)) { + if (provided.equals(BuiltinTypes.INTEGER) || provided.equals(BuiltinTypes.FLOAT)) { return Compatibility.ALWAYS_COMPATIBLE; } } diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePersistance.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePersistance.java index 32b936922f78..0bb25c793f1c 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePersistance.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePersistance.java @@ -11,7 +11,6 @@ @Persistable(clazz = TypeRepresentation.IntersectionType.class, id = 34005) @Persistable(clazz = TypeRepresentation.SumType.class, id = 34006) @Persistable(clazz = TypeRepresentation.UnresolvedSymbol.class, id = 34007) -@Persistable(clazz = AtomTypeInterfaceFromBindingsMap.class, id = 34010) @Persistable(clazz = QualifiedName.class, id = 34012) public final class TypeInferencePersistance { private TypeInferencePersistance() {} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInference.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePropagation.java similarity index 61% rename from engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInference.java rename to engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePropagation.java index ded8086c8862..9a52d3700873 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInference.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferencePropagation.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Objects; +import org.enso.compiler.PackageRepository; import org.enso.compiler.context.InlineContext; import org.enso.compiler.context.ModuleContext; import org.enso.compiler.core.IR; @@ -13,16 +14,18 @@ import org.enso.compiler.pass.IRPass; import org.enso.compiler.pass.IRProcessingPass; import org.enso.compiler.pass.analyse.BindingAnalysis$; +import org.enso.compiler.pass.analyse.types.scope.ModuleResolver; +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScopeAnalysis; import org.enso.compiler.pass.resolve.FullyQualifiedNames$; import org.enso.compiler.pass.resolve.GlobalNames$; import org.enso.compiler.pass.resolve.Patterns$; import org.enso.compiler.pass.resolve.TypeNames$; import org.enso.compiler.pass.resolve.TypeSignatures$; +import org.enso.scala.wrapper.ScalaConversions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import scala.Option; import scala.collection.immutable.Seq; -import scala.collection.immutable.Seq$; -import scala.jdk.javaapi.CollectionConverters; /** * The compiler pass implementing the proof of concept of type inference. @@ -66,32 +69,66 @@ * * @see TypePropagation for more details on the type propagation mechanism */ -public final class TypeInference implements IRPass { - public static final TypeInference INSTANCE = new TypeInference(); - private static final Logger logger = LoggerFactory.getLogger(TypeInference.class); - private final BuiltinTypes builtinTypes = new BuiltinTypes(); +public final class TypeInferencePropagation implements IRPass { + public static final TypeInferencePropagation INSTANCE = new TypeInferencePropagation(); + private static final Logger LOGGER = LoggerFactory.getLogger(TypeInferencePropagation.class); private final TypeResolver typeResolver = new TypeResolver(); - private final TypeCompatibility checker = new TypeCompatibility(builtinTypes); - private final TypePropagation typePropagation = - new TypePropagation(typeResolver, checker, builtinTypes) { - @Override - protected void encounteredIncompatibleTypes( - IR relatedIr, TypeRepresentation expected, TypeRepresentation provided) { - relatedIr - .getDiagnostics() - .add( - new Warning.TypeMismatch( - relatedIr.identifiedLocation(), expected.toString(), provided.toString())); - } - - @Override - protected void encounteredInvocationOfNonFunctionType( - IR relatedIr, TypeRepresentation type) { - relatedIr - .getDiagnostics() - .add(new Warning.NotInvokable(relatedIr.identifiedLocation(), type.toString())); - } - }; + private final TypeCompatibility checker = new TypeCompatibility(); + + private TypeInferencePropagation() {} + + private TypePropagation propagationResolverInModule( + Module module, Option packageRepository) { + var packageRepo = packageRepository.isDefined() ? packageRepository.get() : null; + ModuleResolver moduleResolver = new ModuleResolver(packageRepo); + return new TypePropagation(typeResolver, checker, module, moduleResolver) { + @Override + protected void encounteredIncompatibleTypes( + IR relatedIr, TypeRepresentation expected, TypeRepresentation provided) { + relatedIr + .getDiagnostics() + .add( + new Warning.TypeMismatch( + relatedIr.identifiedLocation(), expected.toString(), provided.toString())); + } + + @Override + protected void encounteredInvocationOfNonFunctionType(IR relatedIr, TypeRepresentation type) { + relatedIr + .getDiagnostics() + .add(new Warning.NotInvokable(relatedIr.identifiedLocation(), type.toString())); + } + + @Override + protected void encounteredNoSuchMethod( + IR relatedIr, TypeRepresentation type, String methodName, MethodCallKind kind) { + String methodDescription = + switch (kind) { + case MEMBER -> "member method `" + methodName + "` on type " + type; + case STATIC -> "static method `" + methodName + "` on " + type; + case MODULE -> "method `" + methodName + "` on module " + type; + }; + relatedIr + .getDiagnostics() + .add(new Warning.NoSuchMethod(relatedIr.identifiedLocation(), methodDescription)); + } + + @Override + protected void encounteredNoSuchConstructor( + IR relatedIr, TypeRepresentation type, String constructorName) { + // TODO make sure if NoSuchMethod is right or we need a separate type here + String methodDescription = "constructor `" + constructorName + "` on type " + type; + relatedIr + .getDiagnostics() + .add(new Warning.NoSuchMethod(relatedIr.identifiedLocation(), methodDescription)); + } + }; + } + + @Override + public String toString() { + return "TypeInferencePropagation"; + } @Override public Seq precursorPasses() { @@ -102,20 +139,22 @@ public Seq precursorPasses() { FullyQualifiedNames$.MODULE$, TypeNames$.MODULE$, Patterns$.MODULE$, - TypeSignatures$.MODULE$); - return CollectionConverters.asScala(passes).toList(); + TypeSignatures$.MODULE$, + StaticModuleScopeAnalysis.INSTANCE, + TypeInferenceSignatures.INSTANCE); + return ScalaConversions.seq(passes); } @Override - @SuppressWarnings("unchecked") public Seq invalidatedPasses() { - return (Seq) Seq$.MODULE$.empty(); + return ScalaConversions.nil(); } @Override public Module runModule(Module ir, ModuleContext moduleContext) { + TypePropagation typePropagation = propagationResolverInModule(ir, moduleContext.pkgRepo()); ir.bindings() - .map( + .foreach( (def) -> switch (def) { case Method.Explicit b -> { @@ -130,16 +169,17 @@ public Module runModule(Module ir, ModuleContext moduleContext) { } case Definition.Type typ -> typ; default -> { - logger.trace("UNEXPECTED definition {}", def.getClass().getCanonicalName()); + LOGGER.trace("UNEXPECTED definition {}", def.getClass().getCanonicalName()); yield def; } }); - return ir; } @Override public Expression runExpression(Expression ir, InlineContext inlineContext) { + var typePropagation = + propagationResolverInModule(inlineContext.getModule().getIr(), inlineContext.pkgRepo()); TypeRepresentation inferredType = typePropagation.tryInferringType(ir, LocalBindingsTyping.create()); if (inferredType != null) { diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferenceSignatures.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferenceSignatures.java new file mode 100644 index 000000000000..48b169cce94a --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeInferenceSignatures.java @@ -0,0 +1,196 @@ +package org.enso.compiler.pass.analyse.types; + +import java.util.List; +import org.enso.compiler.context.InlineContext; +import org.enso.compiler.context.ModuleContext; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.Function; +import org.enso.compiler.core.ir.Module; +import org.enso.compiler.core.ir.Name; +import org.enso.compiler.core.ir.expression.Application; +import org.enso.compiler.core.ir.module.scope.Definition; +import org.enso.compiler.core.ir.module.scope.definition.Method; +import org.enso.compiler.pass.IRPass; +import org.enso.compiler.pass.IRProcessingPass; +import org.enso.compiler.pass.analyse.BindingAnalysis$; +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScopeAnalysis; +import org.enso.compiler.pass.resolve.FullyQualifiedNames$; +import org.enso.compiler.pass.resolve.GlobalNames$; +import org.enso.compiler.pass.resolve.Patterns$; +import org.enso.compiler.pass.resolve.TypeNames$; +import org.enso.compiler.pass.resolve.TypeSignatures$; +import org.enso.scala.wrapper.ScalaConversions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.collection.immutable.Seq; + +// TODO it may make sense to merge this pass into StaticModuleScopeAnalysis, there is little benefit +// to keeping it separate + +/** + * A precursor pass that prepares the IR for type inference, run before the main propagation logic + * runs in {@link TypeInferencePropagation}. + * + *

It handles storing inferred types based on signatures of the top-level bindings. This is done + * as a separate pass, to ensure that once propagation runs, all top-level bindings that have a + * signature, already have an inferred type assigned to them, so that these types can be used. This + * makes the job of the propagation pass much easier, avoiding it to deal with ensuring in what + * order the types are inferred, and ensuring that even without a more complicated unification logic + * that will be needed for recursive definitions, many types can already be inferred. In the future, + * it may be possible that this pass will no longer be needed - if the propagation pass will be + * smart enough to deal with the unknowns. But for now it gives us a very big gain very quickly - as + * most of our standard library is annotated with type signatures, we will be able to benefit from + * these right away, without needing to implement more complicated recursive inference logic. + * + *

This pass is very simple - it looks at ascribed types of the top level bindings to find out + * expected types of each function's arguments, and looks at the outer-most expression of the + * binding in search for a return-type ascription (these ascriptions are inserted by the return-type + * ascription translation in {@code addTypeAscription} within {@link + * org.enso.compiler.core.TreeToIr}). It does not look any deeper into the expressions, ensuring + * that it is relatively quick to run. + */ +public final class TypeInferenceSignatures implements IRPass { + private TypeInferenceSignatures() {} + + public static final TypeInferenceSignatures INSTANCE = new TypeInferenceSignatures(); + private static final Logger logger = LoggerFactory.getLogger(TypeInferenceSignatures.class); + private final TypeResolver typeResolver = new TypeResolver(); + + @Override + public String toString() { + return "TypeInferenceSignatures"; + } + + @Override + public Seq precursorPasses() { + List passes = + List.of( + BindingAnalysis$.MODULE$, + GlobalNames$.MODULE$, + FullyQualifiedNames$.MODULE$, + TypeNames$.MODULE$, + Patterns$.MODULE$, + TypeSignatures$.MODULE$); + return ScalaConversions.seq(passes); + } + + @Override + public Seq invalidatedPasses() { + List passes = + List.of(TypeInferencePropagation.INSTANCE, StaticModuleScopeAnalysis.INSTANCE); + return ScalaConversions.seq(passes); + } + + @Override + public Module runModule(Module ir, ModuleContext moduleContext) { + ir.bindings() + .foreach( + (def) -> + switch (def) { + case Method.Explicit b -> { + boolean keepSelfArgument = b.isStaticWrapperForInstanceMethod(); + TypeRepresentation resolvedType = + resolveTopLevelTypeSignature(b.body(), keepSelfArgument); + if (resolvedType != null) { + b.passData().update(INSTANCE, new InferredType(resolvedType)); + } + yield b; + } + case Definition.Type typ -> typ; + default -> { + logger.trace("UNEXPECTED definition {}", def.getClass().getCanonicalName()); + yield def; + } + }); + return ir; + } + + @Override + public Expression runExpression(Expression ir, InlineContext inlineContext) { + // This pass does not do anything when run on expressions. It only processes top-level bindings. + return ir; + } + + /** + * Constructs the type signature for a given method body. + * + * @param body the method body + * @param keepSelfArgument whether to keep the self argument. For regular static methods, the self + * argument is synthetic and does not take part in type inference. For member methods, it is + * provided implicitly when resolving the call, so again it is ignored for type inference. It + * should only be kept for such static methods that are wrappers for instance methods. + */ + private TypeRepresentation resolveTopLevelTypeSignature( + Expression body, boolean keepSelfArgument) { + return switch (body) { + // Combine argument types with ascribed type (if available) for a function type signature + case Function.Lambda lambda -> { + boolean hasAnyDefaults = + lambda.arguments().find((arg) -> arg.defaultValue().isDefined()).isDefined(); + if (hasAnyDefaults) { + // TODO inferring types that have default arguments + yield null; + } + + scala.collection.immutable.List argTypesScala = + lambda + .arguments() + // Filter out the self argument, unless it should be kept. + .filter( + (arg) -> { + if (arg.name() instanceof Name.Self selfArg) { + if (selfArg.synthetic()) { + // The 'synthetic' self is always dropped. + return false; + } else { + return keepSelfArgument; + } + } else { + // We keep all other args. + return true; + } + }) + .map( + (arg) -> { + if (arg.ascribedType().isDefined()) { + Expression typeExpression = arg.ascribedType().get(); + var resolvedTyp = typeResolver.resolveTypeExpression(typeExpression); + if (resolvedTyp != null) { + return resolvedTyp; + } + } + + return TypeRepresentation.UNKNOWN; + }); + + TypeRepresentation ascribedReturnType = findReturnTypeAscription(lambda.body()); + + if (ascribedReturnType == null && argTypesScala.isEmpty()) { + // If we did not infer return type NOR arity, we know nothing useful about this function, + // so we withdraw. + yield null; + } + + TypeRepresentation returnType = + ascribedReturnType != null ? ascribedReturnType : TypeRepresentation.UNKNOWN; + yield TypeRepresentation.buildFunction(ScalaConversions.asJava(argTypesScala), returnType); + } + + // Otherwise, we encountered a 0-argument method, so its type is just its return type (if + // its known). + default -> findReturnTypeAscription(body); + }; + } + + /** Finds the type ascription associated with the function's return type. */ + private TypeRepresentation findReturnTypeAscription(Expression expression) { + // Special handling for the FORCE node - it wraps the original return value and thus the type + // ascription that was on the outermost expression is no longer outermost - it is now inside of + // the force node, so we need to go 1 level deeper and inspect the target. + if (expression instanceof Application.Force force) { + return typeResolver.getTypeAscription(force.target()); + } + + return typeResolver.getTypeAscription(expression); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypePropagation.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypePropagation.java index 27f254c31679..d8da26588df7 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypePropagation.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypePropagation.java @@ -1,17 +1,17 @@ package org.enso.compiler.pass.analyse.types; import static org.enso.compiler.MetadataInteropHelpers.getMetadata; -import static org.enso.compiler.MetadataInteropHelpers.getMetadataOrNull; import java.util.List; import org.enso.compiler.MetadataInteropHelpers; -import org.enso.compiler.context.NameResolutionAlgorithm; +import org.enso.compiler.common.NameResolutionAlgorithm; import org.enso.compiler.core.CompilerError; import org.enso.compiler.core.IR; import org.enso.compiler.core.ir.CallArgument; import org.enso.compiler.core.ir.Expression; import org.enso.compiler.core.ir.Function; import org.enso.compiler.core.ir.Literal; +import org.enso.compiler.core.ir.Module; import org.enso.compiler.core.ir.Name; import org.enso.compiler.core.ir.Pattern; import org.enso.compiler.core.ir.expression.Application; @@ -21,12 +21,15 @@ import org.enso.compiler.pass.analyse.alias.AliasMetadata; import org.enso.compiler.pass.analyse.alias.graph.Graph; import org.enso.compiler.pass.analyse.alias.graph.GraphOccurrence; -import org.enso.compiler.pass.resolve.TypeSignatures; -import org.enso.compiler.pass.resolve.TypeSignatures$; +import org.enso.compiler.pass.analyse.types.scope.AtomTypeDefinition; +import org.enso.compiler.pass.analyse.types.scope.ModuleResolver; +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScope; +import org.enso.compiler.pass.analyse.types.scope.TypeScopeReference; +import org.enso.pkg.QualifiedName; +import org.enso.scala.wrapper.ScalaConversions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.Option; -import scala.jdk.javaapi.CollectionConverters$; /** * A helper class providing the logic of propagating types through the IR. @@ -40,15 +43,20 @@ abstract class TypePropagation { private static final Logger logger = LoggerFactory.getLogger(TypePropagation.class); private final TypeResolver typeResolver; private final TypeCompatibility compatibilityChecker; - private final BuiltinTypes builtinTypes; + private final ModuleResolver moduleResolver; + private final MethodTypeResolver methodTypeResolver; TypePropagation( TypeResolver typeResolver, TypeCompatibility compatibilityChecker, - BuiltinTypes builtinTypes) { + Module currentModule, + ModuleResolver moduleResolver) { this.typeResolver = typeResolver; this.compatibilityChecker = compatibilityChecker; - this.builtinTypes = builtinTypes; + this.moduleResolver = moduleResolver; + + var currentModuleScope = StaticModuleScope.forIR(currentModule); + this.methodTypeResolver = new MethodTypeResolver(moduleResolver, currentModuleScope); } /** @@ -70,6 +78,26 @@ protected abstract void encounteredIncompatibleTypes( protected abstract void encounteredInvocationOfNonFunctionType( IR relatedIr, TypeRepresentation type); + /** + * The callback that is called when a method is being invoked on a type that does not have such a + * method. + */ + protected abstract void encounteredNoSuchMethod( + IR relatedIr, TypeRepresentation type, String methodName, MethodCallKind kind); + + /** + * The callback that is called when a constructor is being invoked on a type that does not have + * such a constructor. + */ + protected abstract void encounteredNoSuchConstructor( + IR relatedIr, TypeRepresentation type, String constructorName); + + enum MethodCallKind { + MEMBER, + STATIC, + MODULE + } + void checkTypeCompatibility( IR relatedIr, TypeRepresentation expected, TypeRepresentation provided) { TypeCompatibility.Compatibility compatibility = @@ -107,7 +135,7 @@ TypeRepresentation tryInferringType( var bindingType = tryInferringType(b.expression(), localBindingsTyping); if (bindingType != null) { registerBinding(b, bindingType, localBindingsTyping); - TypeInference.setInferredType(b, bindingType); + TypeInferencePropagation.setInferredType(b, bindingType); } yield bindingType; } @@ -119,7 +147,7 @@ TypeRepresentation tryInferringType( } case Function.Lambda f -> processLambda(f, localBindingsTyping); case Literal l -> processLiteral(l); - case Application.Sequence sequence -> builtinTypes.VECTOR; + case Application.Sequence sequence -> BuiltinTypes.VECTOR; case Case.Expr caseExpr -> processCaseExpression(caseExpr, localBindingsTyping); default -> { logger.trace( @@ -128,7 +156,7 @@ TypeRepresentation tryInferringType( } }; - TypeRepresentation ascribedType = findTypeAscription(expression); + TypeRepresentation ascribedType = typeResolver.getTypeAscription(expression); checkInferredAndAscribedTypeCompatibility(expression, inferredType, ascribedType); // We now override the inferred type on the expression, preferring the ascribed type if it is @@ -139,7 +167,7 @@ TypeRepresentation tryInferringType( private TypeRepresentation processCaseExpression( Case.Expr caseExpr, LocalBindingsTyping localBindingsTyping) { List innerTypes = - CollectionConverters$.MODULE$.asJava(caseExpr.branches()).stream() + ScalaConversions.asJava(caseExpr.branches()).stream() .map( branch -> { // Fork the bindings map for each branch, as in the future type equality @@ -169,9 +197,9 @@ private TypeRepresentation processName( private TypeRepresentation processLiteral(Literal literal) { return switch (literal) { case Literal.Number number -> number.isFractional() - ? builtinTypes.FLOAT - : builtinTypes.INTEGER; - case Literal.Text text -> builtinTypes.TEXT; + ? BuiltinTypes.FLOAT + : BuiltinTypes.INTEGER; + case Literal.Text text -> BuiltinTypes.TEXT; // This branch is needed only because Java is unable to infer that the match is exhaustive default -> throw new IllegalStateException( "Impossible - unknown literal type: " + literal.getClass().getCanonicalName()); @@ -230,8 +258,7 @@ private TypeRepresentation processLambda( returnType = TypeRepresentation.UNKNOWN; } - return TypeRepresentation.buildFunction( - CollectionConverters$.MODULE$.asJava(argTypesScala), returnType); + return TypeRepresentation.buildFunction(ScalaConversions.asJava(argTypesScala), returnType); } @SuppressWarnings("unchecked") @@ -286,7 +313,7 @@ private TypeRepresentation processSingleApplication( case TypeRepresentation.UnresolvedSymbol unresolvedSymbol -> { return processUnresolvedSymbolApplication( - unresolvedSymbol, argument.value(), localBindingsTyping); + unresolvedSymbol, argument.value(), localBindingsTyping, relatedIR); } default -> { @@ -306,37 +333,129 @@ private TypeRepresentation processSingleApplication( return null; } + private AtomTypeDefinition findTypeDefinition(QualifiedName name) { + var module = moduleResolver.findContainingModule(TypeScopeReference.atomType(name)); + var moduleScope = StaticModuleScope.forIR(module); + return moduleScope.getType(name.item()); + } + private TypeRepresentation processUnresolvedSymbolApplication( TypeRepresentation.UnresolvedSymbol function, Expression argument, - LocalBindingsTyping localBindingsTyping) { + LocalBindingsTyping localBindingsTyping, + IR relatedWholeApplicationIR) { var argumentType = tryInferringType(argument, localBindingsTyping); if (argumentType == null) { - return null; + argumentType = TypeRepresentation.ANY; } switch (argumentType) { case TypeRepresentation.TypeObject typeObject -> { - var ctorCandidate = - typeObject.typeInterface().constructors().stream() - .filter(ctor -> ctor.name().equals(function.name())) - .findFirst(); - if (ctorCandidate.isPresent()) { - return typeResolver.buildAtomConstructorType(typeObject, ctorCandidate.get()); + if (isConstructorOrType(function.name())) { + return resolveConstructorOnType(typeObject, function.name(), relatedWholeApplicationIR); } else { - // TODO if no ctor found, we should search static methods, but that is not implemented - // currently; so we cannot report an error either - just do nothing + // We resolve static calls on the eigen type. It should also contain registrations of the + // static variants of member methods, so we don't need to inspect member scope. + var staticScope = TypeScopeReference.atomEigenType(typeObject.name()); + var resolvedStaticMethod = methodTypeResolver.resolveMethod(staticScope, function.name()); + if (resolvedStaticMethod == null) { + encounteredNoSuchMethod( + relatedWholeApplicationIR, argumentType, function.name(), MethodCallKind.STATIC); + } + return resolvedStaticMethod; + } + } + + case TypeRepresentation.ModuleReference moduleReference -> { + var typeScope = TypeScopeReference.moduleAssociatedType(moduleReference.name()); + + if (isConstructorOrType(function.name())) { + // This is a special case when we are accessing a type inside a module, e.g. Mod.Type + // 'call' should resolve to the type + // TODO for later return null; + } else { + var resolvedModuleMethod = methodTypeResolver.resolveMethod(typeScope, function.name()); + if (resolvedModuleMethod == null) { + encounteredNoSuchMethod( + relatedWholeApplicationIR, argumentType, function.name(), MethodCallKind.MODULE); + } + return resolvedModuleMethod; + } + } + + case TypeRepresentation.AtomType atomInstanceType -> { + var typeScope = TypeScopeReference.atomType(atomInstanceType.fqn()); + var resolvedMemberMethod = methodTypeResolver.resolveMethod(typeScope, function.name()); + if (resolvedMemberMethod == null) { + encounteredNoSuchMethod( + relatedWholeApplicationIR, argumentType, function.name(), MethodCallKind.MEMBER); + } + return resolvedMemberMethod; + } + + case TypeRepresentation.TopType topType -> { + // We don't report not found methods here, because the top type can be anything, so the call + // 'may' be valid and we only want to report guaranteed failures + return methodTypeResolver.resolveMethod(TypeScopeReference.ANY, function.name()); + } + + // This is not calling this function, instead it is calling the _method_ represented by the + // UnresolvedSymbol on this Function object. + case TypeRepresentation.ArrowType functionAsObject -> { + var typeScope = TypeScopeReference.atomType(functionAsObject.getAssociatedType()); + var resolvedMethod = methodTypeResolver.resolveMethod(typeScope, function.name()); + if (resolvedMethod == null) { + encounteredNoSuchMethod( + relatedWholeApplicationIR, functionAsObject, function.name(), MethodCallKind.MEMBER); } + return resolvedMethod; } default -> { + // TODO calling on sum types, intersection types, etc. + return null; + } + } + } + + private boolean isConstructorOrType(String name) { + assert !name.isEmpty(); + char firstCharacter = name.charAt(0); + return Character.isUpperCase(firstCharacter); + } + + private TypeRepresentation resolveConstructorOnType( + TypeRepresentation.TypeObject typeObject, + String constructorName, + IR relatedWholeApplicationIR) { + assert isConstructorOrType(constructorName); + var typeDefinition = findTypeDefinition(typeObject.name()); + if (typeDefinition == null) { + logger.warn( + "resolveConstructorOnType: {} - no type definition found for {}", + relatedWholeApplicationIR.showCode(), + typeObject.name()); + return null; + } + + var constructor = typeDefinition.getConstructor(constructorName); + if (constructor != null) { + if (constructor.type() == null) { + // type is unknown due to default arguments + // TODO later on this should be assert != null because all constructors should have a + // type (once we can deal with default arguments) return null; } + + return constructor.type(); + } else { + encounteredNoSuchConstructor(relatedWholeApplicationIR, typeObject, constructorName); + return null; } } - private class CompilerNameResolution + private final class CompilerNameResolution extends NameResolutionAlgorithm< TypeRepresentation, CompilerNameResolution.LinkInfo, AliasMetadata.Occurrence> { private final LocalBindingsTyping localBindingsTyping; @@ -359,22 +478,23 @@ protected TypeRepresentation resolveLocalName(LinkInfo localLink) { } @Override - protected TypeRepresentation resolveGlobalName(BindingsMap.ResolvedName resolvedName) { + protected TypeRepresentation resolveGlobalName( + BindingsMap.ResolvedName resolvedName, IR relatedIr) { return switch (resolvedName) { - // TODO investigate when do these appear?? I did not yet see them in the wild case BindingsMap.ResolvedConstructor ctor -> { - var constructorInterface = - new AtomTypeInterfaceFromBindingsMap.ConstructorFromBindingsMap(ctor.cons()); - yield typeResolver.buildAtomConstructorType( - typeResolver.resolvedTypeAsTypeObject(ctor.tpe()), constructorInterface); + var parentType = new TypeRepresentation.TypeObject(ctor.tpe().qualifiedName()); + yield resolveConstructorOnType(parentType, ctor.cons().name(), relatedIr); } case BindingsMap.ResolvedType tpe -> typeResolver.resolvedTypeAsTypeObject(tpe); + case BindingsMap.ResolvedModule mod -> new TypeRepresentation.ModuleReference( + mod.qualifiedName()); + default -> { logger.trace( - "processGlobalName: global scope reference to {} - currently global inference is" - + " unsupported", + "resolveGlobalName: reference to {} - is currently not being resolved in static" + + " analysis", resolvedName); yield null; } @@ -430,7 +550,7 @@ private void registerPattern(Pattern pattern, LocalBindingsTyping localBindingsT registerBinding(typePattern.name(), type, localBindingsTyping); } case Pattern.Constructor constructorPattern -> { - for (var innerPattern : CollectionConverters$.MODULE$.asJava(constructorPattern.fields())) { + for (var innerPattern : ScalaConversions.asJava(constructorPattern.fields())) { registerPattern(innerPattern, localBindingsTyping); } } @@ -438,16 +558,6 @@ private void registerPattern(Pattern pattern, LocalBindingsTyping localBindingsT } } - private TypeRepresentation findTypeAscription(Expression ir) { - TypeSignatures.Signature ascribedSignature = - getMetadataOrNull(ir, TypeSignatures$.MODULE$, TypeSignatures.Signature.class); - if (ascribedSignature != null) { - return typeResolver.resolveTypeExpression(ascribedSignature.signature()); - } else { - return null; - } - } - private void checkInferredAndAscribedTypeCompatibility( Expression ir, TypeRepresentation inferredType, TypeRepresentation ascribedType) { if (ascribedType != null && inferredType != null) { diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeRepresentation.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeRepresentation.java index 46fb63375657..86b7e6e40be3 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeRepresentation.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeRepresentation.java @@ -11,11 +11,12 @@ public sealed interface TypeRepresentation permits TypeRepresentation.ArrowType, TypeRepresentation.AtomType, TypeRepresentation.IntersectionType, + TypeRepresentation.ModuleReference, TypeRepresentation.SumType, TypeRepresentation.TopType, TypeRepresentation.TypeObject, TypeRepresentation.UnresolvedSymbol { - TypeRepresentation ANY = new TopType(); + TopType ANY = new TopType(); // In the future we may want to split this unknown type to be a separate entity. TypeRepresentation UNKNOWN = ANY; @@ -38,6 +39,10 @@ record TopType() implements TypeRepresentation { public String toString() { return "Any"; } + + public QualifiedName getAssociatedType() { + return QualifiedName.fromString(BuiltinTypes.FQN_ANY); + } } /** @@ -46,8 +51,7 @@ public String toString() { *

Instances that are assigned this type are built with one of the available constructors, but * statically we do not necessarily know which one. */ - record AtomType(QualifiedName fqn, AtomTypeInterface typeInterface) - implements TypeRepresentation { + record AtomType(QualifiedName fqn) implements TypeRepresentation { @Override public String toString() { return fqn.item(); @@ -72,7 +76,22 @@ record ArrowType(TypeRepresentation argType, TypeRepresentation resultType) implements TypeRepresentation { @Override public String toString() { - return "(" + argType + " -> " + resultType + ")"; + String arg = argType.toString(); + String res = resultType.toString(); + + // If the inner type is complex (e.g. nested function), wrap it in parentheses. + if (arg.contains(" ")) { + arg = "(" + arg + ")"; + } + if (res.contains(" ")) { + res = "(" + res + ")"; + } + + return arg + " -> " + res; + } + + public QualifiedName getAssociatedType() { + return QualifiedName.fromString(BuiltinTypes.FQN_FUNCTION); } } @@ -137,10 +156,8 @@ public String toString() { * using its constructors, which will be assigned the corresponding AtomType. * * @param name the qualified name of the type - * @param typeInterface the declared interface of the type */ - record TypeObject(QualifiedName name, AtomTypeInterface typeInterface) - implements TypeRepresentation { + record TypeObject(QualifiedName name) implements TypeRepresentation { @Override public String toString() { return "(type " + name.item() + ")"; @@ -159,7 +176,7 @@ public TypeRepresentation instanceType() { return new ArrowType(TypeRepresentation.ANY, TypeRepresentation.ANY); } - return new AtomType(name, typeInterface); + return new AtomType(name); } @Override @@ -177,6 +194,13 @@ public int hashCode() { } } + /** + * A type describing a module. + * + *

This is similar to TypeObject, but one cannot create instances of a module. + */ + record ModuleReference(QualifiedName name) implements TypeRepresentation {} + /** Represents a type of an unresolved symbol, like `.Foo` or `.bar`. */ record UnresolvedSymbol(String name) implements TypeRepresentation { @Override diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeResolver.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeResolver.java index 71892ea24309..1e3ae3c59df0 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeResolver.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/TypeResolver.java @@ -12,7 +12,8 @@ import org.enso.compiler.data.BindingsMap; import org.enso.compiler.pass.resolve.Patterns$; import org.enso.compiler.pass.resolve.TypeNames$; -import org.enso.persist.Persistance; +import org.enso.compiler.pass.resolve.TypeSignatures; +import org.enso.compiler.pass.resolve.TypeSignatures$; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.jdk.javaapi.CollectionConverters; @@ -24,43 +25,10 @@ public class TypeResolver { private static final Logger logger = LoggerFactory.getLogger(TypeResolver.class); - TypeRepresentation resolveTypeExpression(Expression type) { + public TypeRepresentation resolveTypeExpression(Expression type) { return switch (type) { - case Name.Literal name -> { - BindingsMap.Resolution resolutionOrNull = - getMetadataOrNull(name, TypeNames$.MODULE$, BindingsMap.Resolution.class); - - if (resolutionOrNull == null) { - // As fallback, try getting from the Patterns pass. - resolutionOrNull = - getMetadataOrNull(name, Patterns$.MODULE$, BindingsMap.Resolution.class); - } - - if (resolutionOrNull != null) { - BindingsMap.ResolvedName target = resolutionOrNull.target(); - yield switch (target) { - case BindingsMap.ResolvedType resolvedType -> resolvedTypeAsAtomType(resolvedType); - case BindingsMap.ResolvedPolyglotSymbol polyglotSymbol -> { - // for now type inference is not able to deal with polyglot types, so we treat them as - // unknown - yield TypeRepresentation.UNKNOWN; - } - default -> { - logger.debug( - "resolveTypeExpression: {} - unexpected resolved name type {}", - name.showCode(), - target.getClass().getCanonicalName()); - yield TypeRepresentation.UNKNOWN; - } - }; - } else { - // TODO investigate - these seem to unexpectedly come up when compiling Standard.Base - logger.debug( - "resolveTypeExpression: {} - Missing expected TypeName resolution metadata", - type.showCode()); - yield TypeRepresentation.UNKNOWN; - } - } + case Name.Literal name -> getResolvedTypeFromBindingsMap(name); + case Name.SelfType selfType -> getResolvedTypeFromBindingsMap(selfType); case Set.Union union -> { var operands = union.operands().map(this::resolveTypeExpression); @@ -90,11 +58,6 @@ yield switch (target) { // We just ignore the error part for now as it's not really checked anywhere. case Type.Error error -> resolveTypeExpression(error.typed()); - case Name.SelfType selfType -> { - // TODO to be handled in further iterations - yield TypeRepresentation.UNKNOWN; - } - case Name.Qualified qualified -> { // TODO to be handled in further iterations yield TypeRepresentation.UNKNOWN; @@ -121,37 +84,57 @@ yield switch (target) { }; } + private TypeRepresentation getResolvedTypeFromBindingsMap(Name name) { + BindingsMap.Resolution resolutionOrNull = + getMetadataOrNull(name, TypeNames$.MODULE$, BindingsMap.Resolution.class); + + if (resolutionOrNull == null) { + // As fallback, try getting from the Patterns pass. + resolutionOrNull = getMetadataOrNull(name, Patterns$.MODULE$, BindingsMap.Resolution.class); + } + + if (resolutionOrNull != null) { + BindingsMap.ResolvedName target = resolutionOrNull.target(); + return switch (target) { + case BindingsMap.ResolvedType resolvedType -> resolvedTypeAsAtomType(resolvedType); + case BindingsMap.ResolvedPolyglotSymbol polyglotSymbol -> { + // for now type inference is not able to deal with polyglot types, so we treat them as + // unknown + yield TypeRepresentation.UNKNOWN; + } + default -> { + logger.debug( + "resolveTypeExpression: {} - unexpected resolved name type {}", + name.showCode(), + target.getClass().getCanonicalName()); + yield TypeRepresentation.UNKNOWN; + } + }; + } else { + // TODO investigate - these seem to unexpectedly come up when compiling Standard.Base + logger.debug( + "resolveTypeExpression: {} - Missing expected TypeName resolution metadata", + name.showCode()); + return TypeRepresentation.UNKNOWN; + } + } + TypeRepresentation.TypeObject resolvedTypeAsTypeObject(BindingsMap.ResolvedType resolvedType) { - var iface = new AtomTypeInterfaceFromBindingsMap(resolvedType.tp()); - return new TypeRepresentation.TypeObject(resolvedType.qualifiedName(), iface); + return new TypeRepresentation.TypeObject(resolvedType.qualifiedName()); } TypeRepresentation resolvedTypeAsAtomType(BindingsMap.ResolvedType resolvedType) { return resolvedTypeAsTypeObject(resolvedType).instanceType(); } - TypeRepresentation buildAtomConstructorType( - TypeRepresentation.TypeObject parentType, AtomTypeInterface.Constructor constructor) { - boolean hasAnyDefaults = - constructor.arguments().stream().anyMatch(AtomTypeInterface.Argument::hasDefaultValue); - if (hasAnyDefaults) { - // TODO implement handling of default arguments - not only ctors will need this! + /** Returns the type ascribed to the given expression, if any. */ + TypeRepresentation getTypeAscription(Expression ir) { + TypeSignatures.Signature ascribedSignature = + getMetadataOrNull(ir, TypeSignatures$.MODULE$, TypeSignatures.Signature.class); + if (ascribedSignature != null) { + return resolveTypeExpression(ascribedSignature.signature()); + } else { return null; } - - var arguments = - constructor.arguments().stream() - .map( - (arg) -> { - var typ = arg.getType(this); - return typ != null ? typ : TypeRepresentation.UNKNOWN; - }) - .toList(); - var resultType = parentType.instanceType(); - return TypeRepresentation.buildFunction(arguments, resultType); - } - - private TypeRepresentation resolveTypeExpression(Persistance.Reference ref) { - return resolveTypeExpression(ref.get(Expression.class)); } } diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/AtomTypeDefinition.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/AtomTypeDefinition.java new file mode 100644 index 000000000000..445f73fe3337 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/AtomTypeDefinition.java @@ -0,0 +1,43 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import java.util.List; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; + +public final class AtomTypeDefinition { + private final String name; + private final List constructors; + + /** + * Constructs the type definition. + * + * @param name The simple name of the type, e.g. the "Name" in `type Name` + * @param constructors the list of constructor representations + */ + public AtomTypeDefinition(String name, List constructors) { + this.name = name; + this.constructors = constructors; + } + + /** Returns the short name of the type. */ + public String getName() { + return name; + } + + /** + * Returns the constructor of the type with the given name, or {@code null} if a constructor with + * that name does not exist. + */ + public Constructor getConstructor(String name) { + return constructors.stream().filter(c -> c.name().equals(name)).findFirst().orElse(null); + } + + /** + * Represents a constructor of the atom type. + * + * @param name the name of the constructor + * @param isProjectPrivate whether the constructor is project private + * @param type the type ascribed to the constructor, it may be null if it is unknown TODO the type + * will soon be always non-null - once we can handle default arguments + */ + public record Constructor(String name, boolean isProjectPrivate, TypeRepresentation type) {} +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/BuiltinsFallbackScope.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/BuiltinsFallbackScope.java new file mode 100644 index 000000000000..b8186c490ce1 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/BuiltinsFallbackScope.java @@ -0,0 +1,37 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import org.enso.compiler.pass.analyse.types.BuiltinTypes; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.enso.pkg.QualifiedName; + +/** + * This is a special scope that notes methods which are always available on Any type. + * + *

They are available even without any imports. + */ +public final class BuiltinsFallbackScope { + private BuiltinsFallbackScope() {} + + private static final StaticModuleScope fallbackAnyScope = constructFallbackScope(); + + public static StaticModuleScope fallbackAnyScope() { + return fallbackAnyScope; + } + + private static StaticModuleScope constructFallbackScope() { + var scopeBuilder = + new StaticModuleScope.Builder(QualifiedName.fromString("Standard.Builtins.Main")); + scopeBuilder.registerMethod(TypeScopeReference.ANY, "to_text", BuiltinTypes.TEXT); + scopeBuilder.registerMethod(TypeScopeReference.ANY, "to_display_text", BuiltinTypes.TEXT); + scopeBuilder.registerMethod(TypeScopeReference.ANY, "pretty", BuiltinTypes.TEXT); + + var any = new TypeRepresentation.TopType(); + scopeBuilder.registerMethod( + TypeScopeReference.ANY, "==", new TypeRepresentation.ArrowType(any, BuiltinTypes.BOOLEAN)); + + var catchType = + new TypeRepresentation.ArrowType(new TypeRepresentation.ArrowType(any, any), any); + scopeBuilder.registerMethod(TypeScopeReference.ANY, "catch_primitive", catchType); + return scopeBuilder.build(); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/ModuleResolver.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/ModuleResolver.java new file mode 100644 index 000000000000..7bac6ea61d12 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/ModuleResolver.java @@ -0,0 +1,37 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import org.enso.compiler.PackageRepository; +import org.enso.compiler.core.ir.Module; +import org.enso.pkg.QualifiedName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** A helper class that allows to resolve qualified names to loaded modules. */ +public final class ModuleResolver { + private final PackageRepository packageRepository; + private final Logger logger = LoggerFactory.getLogger(ModuleResolver.class); + + public ModuleResolver(PackageRepository packageRepository) { + this.packageRepository = packageRepository; + } + + public Module findModule(QualifiedName name) { + if (packageRepository == null) { + logger.error("Failed to resolve module {} because package repository was null.", name); + return null; + } + + var compilerModuleOpt = packageRepository.getLoadedModule(name.toString()); + if (compilerModuleOpt.isEmpty()) { + return null; + } + + var moduleIr = compilerModuleOpt.get().getIr(); + assert moduleIr != null : "Once a module is found, its IR should be present."; + return moduleIr; + } + + public Module findContainingModule(TypeScopeReference typeScopeReference) { + return findModule(typeScopeReference.relatedModuleName()); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticImportExportScope.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticImportExportScope.java new file mode 100644 index 000000000000..15230d67224f --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticImportExportScope.java @@ -0,0 +1,92 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import org.enso.compiler.common.MethodResolutionAlgorithm; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.enso.pkg.QualifiedName; + +/** The static counterpart of {@link org.enso.interpreter.runtime.scope.ImportExportScope}. */ +public final class StaticImportExportScope { + // TODO add support for only/hiding once https://github.com/enso-org/enso/issues/10796 is fixed + private final QualifiedName referredModuleName; + + public StaticImportExportScope(QualifiedName referredModuleName) { + this.referredModuleName = referredModuleName; + } + + // This field should not be serialized. + private Resolved cachedResolvedScope = null; + + public Resolved resolve( + ModuleResolver moduleResolver, StaticMethodResolution methodResolutionAlgorithm) { + if (cachedResolvedScope != null) { + return cachedResolvedScope; + } + + var module = moduleResolver.findModule(referredModuleName); + if (module == null) { + throw new IllegalStateException("Could not find module: " + referredModuleName); + } + var moduleScope = StaticModuleScope.forIR(module); + var resolved = new Resolved(moduleScope, methodResolutionAlgorithm); + cachedResolvedScope = resolved; + return resolved; + } + + /** + * The resolved version of the import/export scope. + * + *

The qualified name is replaced with the actual reference to the referred scope. + * + *

This value should not be present in the metadata as it is not suitable for serialization. It + * should be constructed ad-hoc whenever needed. + */ + public static class Resolved { + private final StaticModuleScope referredModuleScope; + private final MethodResolutionAlgorithm< + TypeRepresentation, TypeScopeReference, StaticImportExportScope, StaticModuleScope> + methodResolutionAlgorithm; + + private Resolved( + StaticModuleScope moduleScope, + MethodResolutionAlgorithm< + TypeRepresentation, TypeScopeReference, StaticImportExportScope, StaticModuleScope> + methodResolutionAlgorithm) { + this.referredModuleScope = moduleScope; + this.methodResolutionAlgorithm = methodResolutionAlgorithm; + } + + public TypeRepresentation getMethodForType(TypeScopeReference type, String name) { + // TODO filtering only/hiding (see above) - for now we just return everything + return referredModuleScope.getMethodForType(type, name); + } + + public TypeRepresentation getExportedMethod(TypeScopeReference type, String name) { + // TODO filtering only/hiding (see above) - for now we just return everything + return methodResolutionAlgorithm.getExportedMethod(referredModuleScope, type, name); + } + } + + public QualifiedName getReferredModuleName() { + return referredModuleName; + } + + @Override + public String toString() { + return "StaticImportExportScope{" + referredModuleName + "}"; + } + + @Override + public int hashCode() { + return referredModuleName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StaticImportExportScope other)) { + return false; + } + + // TODO once hiding (see above) is added, these filters need to be added too + return referredModuleName.equals(other.referredModuleName); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticMethodResolution.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticMethodResolution.java new file mode 100644 index 000000000000..39aed5259a8b --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticMethodResolution.java @@ -0,0 +1,118 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import java.util.Collection; +import java.util.List; +import org.enso.compiler.common.MethodResolutionAlgorithm; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** The implementation of {@link MethodResolutionAlgorithm} for static analysis. */ +public final class StaticMethodResolution + extends MethodResolutionAlgorithm< + TypeRepresentation, TypeScopeReference, StaticImportExportScope, StaticModuleScope> { + private final ModuleResolver moduleResolver; + private static final Logger LOGGER = LoggerFactory.getLogger(StaticMethodResolution.class); + + public StaticMethodResolution(ModuleResolver moduleResolver) { + this.moduleResolver = moduleResolver; + } + + @Override + protected Collection getImportsFromModuleScope( + StaticModuleScope moduleScope) { + return moduleScope.getImports(); + } + + @Override + protected Collection getExportsFromModuleScope( + StaticModuleScope moduleScope) { + return moduleScope.getExports(); + } + + @Override + protected TypeRepresentation getConversionFromModuleScope( + StaticModuleScope moduleScope, TypeScopeReference target, TypeScopeReference source) { + return moduleScope.getConversionFor(target, source); + } + + @Override + protected TypeRepresentation getMethodFromModuleScope( + StaticModuleScope moduleScope, TypeScopeReference typeScopeReference, String methodName) { + return moduleScope.getMethodForType(typeScopeReference, methodName); + } + + @Override + protected StaticModuleScope findDefinitionScope(TypeScopeReference typeScopeReference) { + var definitionModule = moduleResolver.findContainingModule(typeScopeReference); + if (definitionModule != null) { + return StaticModuleScope.forIR(definitionModule); + } else { + if (typeScopeReference.equals(TypeScopeReference.ANY)) { + // We have special handling for ANY: it points to Standard.Base.Any.Any, but that may not + // always be imported. + // The runtime falls back to Standard.Builtins.Main, but that modules does not contain any + // type information, so it is not useful for us. + // Instead we fall back to the hardcoded definitions of the 5 builtins of Any. + return BuiltinsFallbackScope.fallbackAnyScope(); + } else { + LOGGER.error("Could not find declaration module of type: {}", typeScopeReference); + return null; + } + } + } + + @Override + protected TypeRepresentation getMethodForTypeFromScope( + StaticImportExportScope scope, TypeScopeReference typeScopeReference, String methodName) { + return scope.resolve(moduleResolver, this).getMethodForType(typeScopeReference, methodName); + } + + @Override + protected TypeRepresentation getExportedMethodFromScope( + StaticImportExportScope scope, TypeScopeReference typeScopeReference, String methodName) { + return scope.resolve(moduleResolver, this).getExportedMethod(typeScopeReference, methodName); + } + + @Override + protected TypeRepresentation getConversionFromScope( + StaticImportExportScope scope, TypeScopeReference target, TypeScopeReference source) { + // TODO conversions in static analysis + return null; + } + + @Override + protected TypeRepresentation getExportedConversionFromScope( + StaticImportExportScope scope, TypeScopeReference target, TypeScopeReference source) { + // TODO conversions in static analysis + return null; + } + + @Override + protected TypeRepresentation onMultipleDefinitionsFromImports( + String methodName, + List> methodFromImports) { + if (LOGGER.isDebugEnabled()) { + var foundImportNames = methodFromImports.stream().map(MethodFromImport::origin); + LOGGER.debug("Method {} is coming from multiple imports: {}", methodName, foundImportNames); + } + + long foundTypesCount = + methodFromImports.stream().map(MethodFromImport::resolutionResult).distinct().count(); + if (foundTypesCount > 1) { + List foundTypesWithOrigins = + methodFromImports.stream() + .distinct() + .map(m -> m.resolutionResult() + " from " + m.origin()) + .toList(); + LOGGER.error( + "Method {} is coming from multiple imports with different types: {}", + methodName, + foundTypesWithOrigins); + return null; + } else { + // If all types are the same, just return the first one + return methodFromImports.get(0).resolutionResult(); + } + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScope.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScope.java new file mode 100644 index 000000000000..db0841a6a528 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScope.java @@ -0,0 +1,174 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.enso.compiler.MetadataInteropHelpers; +import org.enso.compiler.core.CompilerStub; +import org.enso.compiler.core.ir.Module; +import org.enso.compiler.core.ir.ProcessingPass; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.enso.pkg.QualifiedName; +import scala.Option; + +/** + * This is a sibling to the ModuleScope. + * + *

The ModuleScope is the runtime representation of a module, optimized for fast runtime + * dispatch. The StaticModuleScope is an analogous structure, that can be used by static analysis + * passes at compilation time. + * + *

It is also similar to the BindingsMap structure. In fact, it may be possible to merge the two + * modules in the future, as StaticModuleScope is a more general variant. The BindingsMap only deals + * with Types and their Constructors that are used during static resolution of some names. This + * class also keeps track of all defined methods, to facilitate type checking. I'm keeping these + * separate for now as it is easier to create a prototype that way. If later we find out they have + * enough of similarity, we should merge them. + */ +public final class StaticModuleScope implements ProcessingPass.Metadata { + private final QualifiedName moduleName; + private final TypeScopeReference associatedType; + private final List imports; + private final List exports; + private final Map typesDefinedHere; + private final Map> methods; + + private StaticModuleScope( + QualifiedName moduleName, + TypeScopeReference associatedType, + List imports, + List exports, + Map typesDefinedHere, + Map> methods) { + this.moduleName = moduleName; + this.associatedType = associatedType; + this.imports = imports; + this.exports = exports; + this.typesDefinedHere = typesDefinedHere; + this.methods = methods; + } + + static final class Builder { + private final QualifiedName moduleName; + private final TypeScopeReference associatedType; + private final List imports = new ArrayList<>(); + private final List exports = new ArrayList<>(); + private final Map typesDefinedHere = new HashMap<>(); + private final Map> methods = + new HashMap<>(); + + private boolean sealed = false; + + private void checkSealed() { + if (sealed) { + throw new IllegalStateException( + "`build` method has already been called, this builder should no longer be modified."); + } + } + + Builder(QualifiedName moduleName) { + this.moduleName = moduleName; + this.associatedType = TypeScopeReference.moduleAssociatedType(moduleName); + } + + public StaticModuleScope build() { + sealed = true; + return new StaticModuleScope( + moduleName, + associatedType, + Collections.unmodifiableList(imports), + Collections.unmodifiableList(exports), + Collections.unmodifiableMap(typesDefinedHere), + Collections.unmodifiableMap(methods)); + } + + QualifiedName getModuleName() { + return moduleName; + } + + public TypeScopeReference getAssociatedType() { + return associatedType; + } + + void registerType(AtomTypeDefinition type) { + checkSealed(); + var previous = typesDefinedHere.putIfAbsent(type.getName(), type); + if (previous != null) { + throw new IllegalStateException("Type already defined: " + type.getName()); + } + } + + void registerMethod(TypeScopeReference parentType, String name, TypeRepresentation type) { + checkSealed(); + var typeMethods = methods.computeIfAbsent(parentType, k -> new HashMap<>()); + typeMethods.put(name, type); + } + + public void addImport(StaticImportExportScope importScope) { + checkSealed(); + imports.add(importScope); + } + + public void addExport(StaticImportExportScope exportScope) { + checkSealed(); + exports.add(exportScope); + } + } + + public TypeScopeReference getAssociatedType() { + return associatedType; + } + + public static StaticModuleScope forIR(Module module) { + return MetadataInteropHelpers.getMetadata( + module, StaticModuleScopeAnalysis.INSTANCE, StaticModuleScope.class); + } + + public TypeRepresentation getMethodForType(TypeScopeReference type, String name) { + var typeMethods = methods.get(type); + if (typeMethods == null) { + return null; + } + + return typeMethods.get(name); + } + + @Override + public String metadataName() { + return "StaticModuleScope"; + } + + @Override + public ProcessingPass.Metadata prepareForSerialization(CompilerStub compiler) { + return this; + } + + @Override + public Option restoreFromSerialization(CompilerStub compiler) { + return Option.apply(this); + } + + @Override + public Option duplicate() { + return Option.empty(); + } + + public List getImports() { + return imports; + } + + public List getExports() { + return exports; + } + + public TypeRepresentation getConversionFor(TypeScopeReference target, TypeScopeReference source) { + // TODO conversions in static analysis + return null; + } + + public AtomTypeDefinition getType(String name) { + return typesDefinedHere.get(name); + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScopeAnalysis.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScopeAnalysis.java new file mode 100644 index 000000000000..c31612ac106e --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/StaticModuleScopeAnalysis.java @@ -0,0 +1,245 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import static org.enso.compiler.MetadataInteropHelpers.getMetadata; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.enso.compiler.MetadataInteropHelpers; +import org.enso.compiler.common.BuildScopeFromModuleAlgorithm; +import org.enso.compiler.context.InlineContext; +import org.enso.compiler.context.ModuleContext; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.Module; +import org.enso.compiler.core.ir.module.scope.Definition; +import org.enso.compiler.core.ir.module.scope.definition.Method; +import org.enso.compiler.data.BindingsMap; +import org.enso.compiler.pass.IRPass; +import org.enso.compiler.pass.IRProcessingPass; +import org.enso.compiler.pass.analyse.BindingAnalysis$; +import org.enso.compiler.pass.analyse.types.InferredType; +import org.enso.compiler.pass.analyse.types.TypeInferencePropagation; +import org.enso.compiler.pass.analyse.types.TypeInferenceSignatures; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.enso.compiler.pass.analyse.types.TypeResolver; +import org.enso.compiler.pass.resolve.FullyQualifiedNames$; +import org.enso.compiler.pass.resolve.GlobalNames$; +import org.enso.compiler.pass.resolve.TypeNames$; +import org.enso.pkg.QualifiedName; +import org.enso.scala.wrapper.ScalaConversions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.Option; +import scala.collection.immutable.Seq; + +public class StaticModuleScopeAnalysis implements IRPass { + public static final StaticModuleScopeAnalysis INSTANCE = new StaticModuleScopeAnalysis(); + private final Logger logger = LoggerFactory.getLogger(StaticModuleScopeAnalysis.class); + + private final TypeResolver typeResolver = new TypeResolver(); + + private StaticModuleScopeAnalysis() {} + + @Override + public String toString() { + return "StaticModuleScopeAnalysis"; + } + + @Override + public Seq precursorPasses() { + List passes = + List.of( + GlobalNames$.MODULE$, + BindingAnalysis$.MODULE$, + FullyQualifiedNames$.MODULE$, + TypeNames$.MODULE$, + TypeInferenceSignatures.INSTANCE); + return ScalaConversions.seq(passes); + } + + @Override + public Seq invalidatedPasses() { + List passes = List.of(TypeInferencePropagation.INSTANCE); + return ScalaConversions.seq(passes); + } + + @Override + public Module runModule(Module ir, ModuleContext moduleContext) { + // This has a lot in common with IrToTruffle::processModule - we may want to extract some common + // parts if it will make sense. + StaticModuleScope.Builder scopeBuilder = new StaticModuleScope.Builder(moduleContext.getName()); + BindingsMap bindingsMap = getMetadata(ir, BindingAnalysis$.MODULE$, BindingsMap.class); + + BuildStaticModuleScope buildScopeAlgorithm = new BuildStaticModuleScope(scopeBuilder); + buildScopeAlgorithm.processModule(ir, bindingsMap); + StaticModuleScope scope = scopeBuilder.build(); + ir.passData().update(INSTANCE, scope); + return ir; + } + + @Override + public Expression runExpression(Expression ir, InlineContext inlineContext) { + // Nothing to do - this pass only works on module-level. + return ir; + } + + private final class BuildStaticModuleScope + extends BuildScopeFromModuleAlgorithm { + private StaticModuleScope.Builder scopeBuilder; + + private BuildStaticModuleScope(StaticModuleScope.Builder scopeBuilder) { + this.scopeBuilder = scopeBuilder; + } + + @Override + protected void registerExport(StaticImportExportScope exportScope) { + scopeBuilder.addExport(exportScope); + } + + @Override + protected void registerImport(StaticImportExportScope importScope) { + scopeBuilder.addImport(importScope); + } + + @Override + protected TypeScopeReference getTypeAssociatedWithCurrentScope() { + return scopeBuilder.getAssociatedType(); + } + + @Override + protected void processPolyglotJavaImport(String visibleName, String javaClassName) { + // Currently nothing to do here, as we don't resolve methods on Java types. Assigning them + // with Any should be good enough. + // TODO: we may want a test making sure that we don't do any false positive warnings + } + + @Override + protected void processConversion(Method.Conversion conversion) { + // TODO conversion handling is not implemented yet in the type checker + } + + @Override + protected void processMethodDefinition(Method.Explicit method) { + var typeScope = getTypeDefiningMethod(method); + if (typeScope == null) { + logger.warn( + "Failed to process method {}, because its type scope could not be resolved.", + method.methodReference().showCode()); + return; + } + var typeFromSignature = + MetadataInteropHelpers.getMetadataOrNull( + method, TypeInferenceSignatures.INSTANCE, InferredType.class); + var type = typeFromSignature != null ? typeFromSignature.type() : TypeRepresentation.UNKNOWN; + var name = method.methodReference().methodName().name(); + scopeBuilder.registerMethod(typeScope, name, type); + } + + @Override + protected void processTypeDefinition(Definition.Type typ) { + QualifiedName qualifiedName = scopeBuilder.getModuleName().createChild(typ.name().name()); + TypeRepresentation.TypeObject typeObject = new TypeRepresentation.TypeObject(qualifiedName); + List constructors = + ScalaConversions.asJava(typ.members()).stream() + .map( + constructorDef -> { + TypeRepresentation type = buildAtomConstructorType(typeObject, constructorDef); + return new AtomTypeDefinition.Constructor( + constructorDef.name().name(), constructorDef.isPrivate(), type); + }) + .toList(); + + AtomTypeDefinition atomTypeDefinition = + new AtomTypeDefinition(typ.name().name(), constructors); + var atomTypeScope = TypeScopeReference.atomType(qualifiedName); + scopeBuilder.registerType(atomTypeDefinition); + registerFieldGetters(scopeBuilder, atomTypeScope, typ); + } + + private TypeRepresentation buildAtomConstructorType( + TypeRepresentation.TypeObject associatedType, Definition.Data constructorDef) { + boolean hasDefaults = constructorDef.arguments().exists(a -> a.defaultValue().isDefined()); + if (hasDefaults) { + // TODO implement handling of default arguments - not only ctors will need this! + return null; + } + + var arguments = + constructorDef + .arguments() + .map( + (arg) -> { + Option typ = arg.ascribedType(); + if (typ.isEmpty()) { + return TypeRepresentation.UNKNOWN; + } + + var resolvedType = typeResolver.resolveTypeExpression(typ.get()); + assert resolvedType != null; + return resolvedType; + }) + .toList(); + var resultType = associatedType.instanceType(); + return TypeRepresentation.buildFunction(ScalaConversions.asJava(arguments), resultType); + } + + @Override + protected TypeScopeReference associatedTypeFromResolvedModule( + BindingsMap.ResolvedModule module) { + return TypeScopeReference.moduleAssociatedType(module.qualifiedName()); + } + + @Override + protected TypeScopeReference associatedTypeFromResolvedType( + BindingsMap.ResolvedType type, boolean isStatic) { + return TypeScopeReference.atomType(type.qualifiedName(), isStatic); + } + + @Override + protected StaticImportExportScope buildExportScope(BindingsMap.ExportedModule exportedModule) { + return new StaticImportExportScope(exportedModule.module().qualifiedName()); + } + + @Override + protected StaticImportExportScope buildImportScope( + BindingsMap.ResolvedImport resolvedImport, BindingsMap.ResolvedModule resolvedModule) { + return new StaticImportExportScope(resolvedModule.qualifiedName()); + } + } + + @Override + public T updateMetadataInDuplicate(T sourceIr, T copyOfIr) { + return IRPass.super.updateMetadataInDuplicate(sourceIr, copyOfIr); + } + + /** + * Registers getters for fields of the given type. + * + *

This should be consistent with logic with AtomConstructor.collectFieldAccessors. + */ + private void registerFieldGetters( + StaticModuleScope.Builder scope, + TypeScopeReference typeScope, + Definition.Type typeDefinition) { + Map> fieldTypes = new HashMap<>(); + for (var constructorDef : ScalaConversions.asJava(typeDefinition.members())) { + for (var argumentDef : ScalaConversions.asJava(constructorDef.arguments())) { + String fieldName = argumentDef.name().name(); + TypeRepresentation fieldType = + argumentDef + .ascribedType() + .map(typeResolver::resolveTypeExpression) + .getOrElse(() -> TypeRepresentation.UNKNOWN); + fieldTypes.computeIfAbsent(fieldName, k -> new ArrayList<>()).add(fieldType); + } + } + + for (var entry : fieldTypes.entrySet()) { + String fieldName = entry.getKey(); + TypeRepresentation mergedType = TypeRepresentation.buildSimplifiedSumType(entry.getValue()); + scope.registerMethod(typeScope, fieldName, mergedType); + } + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeHierarchy.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeHierarchy.java new file mode 100644 index 000000000000..3036fc71d890 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeHierarchy.java @@ -0,0 +1,42 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import org.enso.compiler.pass.analyse.types.BuiltinTypes; +import org.enso.pkg.QualifiedName; + +/** + * A helper that encapsulates the hierarchy of types. + * + *

    + *
  • Module types and Any, have no parents. + *
  • The types Integer and Float have Number as their parent. + *
  • Any other type has Any as its parent. + *
+ * + * This should be aligned with Type.allTypes in the interpreter. + */ +public class TypeHierarchy { + private TypeHierarchy() {} + + public static TypeScopeReference getParent(TypeScopeReference type) { + switch (type.getKind()) { + case MODULE_ASSOCIATED_TYPE: + return null; + case ATOM_TYPE: + var name = type.getName(); + if (BuiltinTypes.isAny(name)) { + // Any has no more parents + return null; + } + + if (BuiltinTypes.isInteger(name) || BuiltinTypes.isFloat(name)) { + return TypeScopeReference.atomType(QualifiedName.fromString(BuiltinTypes.FQN_NUMBER)); + } + + return TypeScopeReference.ANY; + case ATOM_EIGEN_TYPE: + return TypeScopeReference.ANY; + default: + throw new RuntimeException("Unknown type kind: " + type.getKind()); + } + } +} diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeScopeReference.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeScopeReference.java new file mode 100644 index 000000000000..8361151385a1 --- /dev/null +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/pass/analyse/types/scope/TypeScopeReference.java @@ -0,0 +1,96 @@ +package org.enso.compiler.pass.analyse.types.scope; + +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.enso.pkg.QualifiedName; + +/** + * An identity of a Type (corresponds to {@link org.enso.interpreter.runtime.data.Type}). + * + *

It can be one of three things: + * + *

    + *
  • An atom type + *
  • An eigentype of an atom type - this will hold the type's static methods + *
  • An associated type of a module + *
+ */ +public final class TypeScopeReference { + private final QualifiedName name; + + private final Kind kind; + + private TypeScopeReference(QualifiedName name, Kind kind) { + this.name = name; + this.kind = kind; + } + + public static TypeScopeReference moduleAssociatedType(QualifiedName moduleName) { + return new TypeScopeReference(moduleName, Kind.MODULE_ASSOCIATED_TYPE); + } + + public static TypeScopeReference atomType(QualifiedName atomTypeName) { + return new TypeScopeReference(atomTypeName, Kind.ATOM_TYPE); + } + + public static TypeScopeReference atomEigenType(QualifiedName atomTypeName) { + return new TypeScopeReference(atomTypeName, Kind.ATOM_EIGEN_TYPE); + } + + public static TypeScopeReference atomType(QualifiedName atomTypeName, boolean staticCall) { + return staticCall ? atomEigenType(atomTypeName) : atomType(atomTypeName); + } + + @Override + public int hashCode() { + return name.hashCode() + kind.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TypeScopeReference other)) { + return false; + } + + return name.equals(other.name) && kind.equals(other.kind); + } + + enum Kind { + MODULE_ASSOCIATED_TYPE, + ATOM_TYPE, + ATOM_EIGEN_TYPE + } + + QualifiedName getName() { + return name; + } + + QualifiedName relatedModuleName() { + switch (kind) { + case MODULE_ASSOCIATED_TYPE: + return name; + case ATOM_TYPE: + case ATOM_EIGEN_TYPE: + var parent = name.getParent(); + assert parent.isDefined(); + return parent.get(); + default: + throw new IllegalStateException("Unexpected value: " + kind); + } + } + + Kind getKind() { + return kind; + } + + @Override + public String toString() { + return switch (kind) { + case MODULE_ASSOCIATED_TYPE -> "ModuleAssociatedType(" + name + ")"; + case ATOM_TYPE -> "AtomType(" + name + ")"; + case ATOM_EIGEN_TYPE -> "AtomEigenType(" + name + ")"; + }; + } + + public static TypeScopeReference ANY = + TypeScopeReference.atomType(TypeRepresentation.ANY.getAssociatedType()); +} diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/Compiler.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/Compiler.scala index 467306ebfbf5..3484c211d670 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/Compiler.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/Compiler.scala @@ -133,12 +133,17 @@ class Compiler( * * @param shouldCompileDependencies whether compilation should also compile * the dependencies of the requested package + * @param shouldWriteCache whether the compilation results should be written + * to the cache; if set to False, a 'lint' compilation + * will be performed, reporting any problems, + * but no results will be written * @param useGlobalCacheLocations whether or not the compilation result should * be written to the global cache * @return future to track subsequent serialization of the library */ def compile( shouldCompileDependencies: Boolean, + shouldWriteCache: Boolean, useGlobalCacheLocations: Boolean ): Future[java.lang.Boolean] = { getPackageRepository.getMainProjectPackage match { @@ -181,11 +186,13 @@ class Compiler( shouldCompileDependencies ) - context.serializeLibrary( - this, - pkg.libraryName, - useGlobalCacheLocations - ) + if (shouldWriteCache) { + context.serializeLibrary( + this, + pkg.libraryName, + useGlobalCacheLocations + ) + } else CompletableFuture.completedFuture(true) } } } @@ -358,6 +365,33 @@ class Compiler( } } + requiredModules.foreach { module => + if ( + !context + .getCompilationStage(module) + .isAtLeast( + CompilationStage.AFTER_TYPE_INFERENCE_PASSES + ) + ) { + + val moduleContext = ModuleContext( + module = module, + freshNameSupply = Some(freshNameSupply), + compilerConfig = config, + pkgRepo = Some(packageRepository) + ) + val compilerOutput = + runFinalTypeInferencePasses(context.getIr(module), moduleContext) + context.updateModule( + module, + { u => + u.ir(compilerOutput) + u.compilationStage(CompilationStage.AFTER_TYPE_INFERENCE_PASSES) + } + ) + } + } + runErrorHandling(requiredModules) val requiredModulesWithScope = requiredModules.map { module => @@ -808,6 +842,21 @@ class Compiler( passManager.runPassesOnModule(ir, moduleContext, passes.globalTypingPasses) } + /** Runs the final type inference passes, if they are enabled. + * + * If they are not enabled, it will not run any passes. + */ + private def runFinalTypeInferencePasses( + ir: IRModule, + moduleContext: ModuleContext + ): IRModule = { + passManager.runPassesOnModule( + ir, + moduleContext, + passes.typeInferenceFinalPasses + ) + } + /** Runs the various compiler passes in an inline context. * * @param ir the compiler intermediate representation to transform diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/Passes.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/Passes.scala index d9f75cba68eb..9d0f42809031 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/Passes.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/Passes.scala @@ -4,7 +4,11 @@ import org.enso.compiler.data.CompilerConfig import org.enso.compiler.dump.IRDumperPass import org.enso.compiler.pass.PassConfiguration._ import org.enso.compiler.pass.analyse._ -import org.enso.compiler.pass.analyse.types.TypeInference +import org.enso.compiler.pass.analyse.types.scope.StaticModuleScopeAnalysis +import org.enso.compiler.pass.analyse.types.{ + TypeInferencePropagation, + TypeInferenceSignatures +} import org.enso.compiler.pass.desugar._ import org.enso.compiler.pass.lint.{ ModuleNameConflicts, @@ -102,13 +106,22 @@ class Passes(config: CompilerConfig) { List(UnusedBindings, NoSelfInStatic) }) ++ (if (config.staticTypeInferenceEnabled) { List( - TypeInference.INSTANCE + TypeInferenceSignatures.INSTANCE, + StaticModuleScopeAnalysis.INSTANCE ) } else Nil) ++ (if (config.dumpIrs) { List(IRDumperPass.INSTANCE) } else Nil) ) + val typeInferenceFinalPasses = new PassGroup( + if (config.staticTypeInferenceEnabled) { + List( + TypeInferencePropagation.INSTANCE + ) + } else List() + ) + /** A list of the compiler phases, in the order they should be run. * * The pass manager checks at runtime whether the provided order respects the @@ -119,7 +132,8 @@ class Passes(config: CompilerConfig) { List( moduleDiscoveryPasses, globalTypingPasses, - functionBodyPasses + functionBodyPasses, + typeInferenceFinalPasses ) /** The ordered representation of all passes run by the compiler. */ diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/context/InlineContext.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/context/InlineContext.scala index af96f978204a..2ae1db75f164 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/context/InlineContext.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/context/InlineContext.scala @@ -8,7 +8,7 @@ import org.enso.compiler.pass.PassConfiguration /** A type containing the information about the execution context for an inline * expression. * - * @param module the module in which the expression is being executed + * @param moduleContext the module in which the expression is being executed * @param compilerConfig the compiler configuration * @param localScope the local scope in which the expression is being executed * @param isInTailPosition whether or not the inline expression occurs in tail @@ -18,7 +18,7 @@ import org.enso.compiler.pass.PassConfiguration * @param pkgRepo the compiler's package repository */ case class InlineContext( - private val module: ModuleContext, + private val moduleContext: ModuleContext, compilerConfig: CompilerConfig, localScope: Option[LocalScope] = None, isInTailPosition: Option[Boolean] = None, @@ -26,14 +26,13 @@ case class InlineContext( passConfiguration: Option[PassConfiguration] = None, pkgRepo: Option[PackageRepository] = None ) extends AutoCloseable { - def bindingsAnalysis(): BindingsMap = module.bindingsAnalysis() - def getModule() = module.module + def bindingsAnalysis(): BindingsMap = moduleContext.bindingsAnalysis() + def getModule() = moduleContext.module def close(): Unit = { this.localScope .foreach(_.scope.removeScopeFromParent()) } - } object InlineContext { @@ -57,7 +56,7 @@ object InlineContext { ): InlineContext = { InlineContext( localScope = Option(localScope), - module = ModuleContext(module, compilerConfig), + moduleContext = ModuleContext(module, compilerConfig), isInTailPosition = isInTailPosition, compilerConfig = compilerConfig, pkgRepo = pkgRepo @@ -73,7 +72,7 @@ object InlineContext { def fromModuleContext(moduleContext: ModuleContext): InlineContext = { InlineContext( localScope = None, - module = moduleContext, + moduleContext = moduleContext, isInTailPosition = None, freshNameSupply = moduleContext.freshNameSupply, passConfiguration = moduleContext.passConfiguration, diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala index 9c8753aad043..c9df1f19a5cb 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala @@ -4,18 +4,15 @@ 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 -import org.enso.compiler.core.ir +import org.enso.compiler.core.{ir, CompilerError} import org.enso.compiler.core.ir.expression.errors -import org.enso.compiler.data.BindingsMap.{DefinedEntity, ModuleReference} -import org.enso.compiler.core.CompilerError -import org.enso.compiler.core.ir.Expression import org.enso.compiler.core.ir.module.scope.Definition +import org.enso.compiler.data.BindingsMap.{DefinedEntity, ModuleReference} import org.enso.compiler.pass.IRPass import org.enso.compiler.pass.analyse.BindingAnalysis import org.enso.compiler.pass.resolve.MethodDefinitions -import org.enso.persist.Persistance.Reference -import org.enso.pkg.QualifiedName import org.enso.editions.LibraryName +import org.enso.pkg.QualifiedName import java.io.ObjectOutputStream import scala.annotation.unused @@ -595,15 +592,7 @@ object BindingsMap { def allFieldsDefaulted: Boolean = arguments.forall(_.hasDefaultValue) } - case class Argument( - name: String, - hasDefaultValue: Boolean, - typReference: Reference[Expression] - ) { - def typ(): Option[Expression] = Option( - typReference.get(classOf[Expression]) - ) - } + case class Argument(name: String, hasDefaultValue: Boolean) /** A representation of a sum type * @@ -629,15 +618,9 @@ object BindingsMap { Cons( m.name.name, m.arguments.map { arg => - val ascribedType: Reference[Expression] = - arg.ascribedType match { - case Some(value) => Reference.of(value, true) - case None => Reference.none() - } BindingsMap.Argument( arg.name.name, - arg.defaultValue.isDefined, - ascribedType + arg.defaultValue.isDefined ) }, m.isPrivate diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/PassManager.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/PassManager.scala index 9c643deeb200..8f0bbeaa99ea 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/PassManager.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/PassManager.scala @@ -1,11 +1,9 @@ package org.enso.compiler.pass -import org.enso.common.{Asserts, CompilationStage} import org.slf4j.LoggerFactory import org.enso.compiler.context.{InlineContext, ModuleContext} import org.enso.compiler.core.ir.{Expression, Module} import org.enso.compiler.core.{CompilerError, IR} -import org.enso.compiler.pass.analyse.BindingAnalysis import scala.collection.mutable.ListBuffer @@ -73,8 +71,6 @@ class PassManager( moduleContext: ModuleContext, passGroup: PassGroup ): Module = { - Asserts.assertInJvm(validateConsistency(ir, moduleContext)) - if (!passes.contains(passGroup)) { throw new CompilerError("Cannot run an unvalidated pass group.") } @@ -247,51 +243,6 @@ class PassManager( ix - totalLength == indexOfPassInGroup } - /** Validates consistency between the IR accessible via `moduleContext` and `ir`. - * There is no way to enforce this consistency statically. - * Should be called only iff assertions are enabled. - * @return true if they are consistent, otherwise throws [[AssertionError]]. - */ - private def validateConsistency( - ir: Module, - moduleContext: ModuleContext - ): Boolean = { - def hex(obj: Object): String = { - if (obj != null) { - val hexStr = Integer.toHexString(System.identityHashCode(obj)) - val className = obj.getClass.getSimpleName - s"$className@${hexStr}" - } else { - "null" - } - } - - if ( - moduleContext.module.getCompilationStage.isAtLeast( - CompilationStage.AFTER_PARSING - ) - ) { - if (!(moduleContext.module.getIr eq ir)) { - throw new AssertionError( - "Mismatch of IR between ModuleContext and IR in module '" + moduleContext - .getName() + "'. " + - s"IR from moduleContext: ${hex(moduleContext.module.getIr)}, IR from module: ${hex(ir)}" - ) - } - val bmFromCtx = moduleContext.bindingsAnalysis() - val bmFromMeta = ir.passData.get(BindingAnalysis) - if (bmFromMeta.isDefined || bmFromCtx != null) { - Asserts.assertInJvm( - bmFromCtx eq bmFromMeta.get, - s"BindingsMap mismatch between ModuleContext and IR in module '" + - moduleContext.getName() + "'. " + - s"BindingsMap from moduleContext: ${hex(bmFromCtx)}, BindingsMap from IR: ${hex(bmFromMeta.get)}" - ) - } - } - true - } - /** Updates the metadata in a copy of the IR when updating that metadata * requires global state. * diff --git a/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/CompilerTestSetup.scala b/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/CompilerTestSetup.scala index 216260e9eca1..62edc72e4fcf 100644 --- a/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/CompilerTestSetup.scala +++ b/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/CompilerTestSetup.scala @@ -194,7 +194,7 @@ trait CompilerTestSetup { compilerConfig = compilerConfig ) InlineContext( - module = mc, + moduleContext = mc, freshNameSupply = freshNameSupply, passConfiguration = passConfiguration, localScope = localScope, diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerdeCompilerTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerdeCompilerTest.java index d070c83bc253..437419b7a6e4 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerdeCompilerTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerdeCompilerTest.java @@ -69,7 +69,7 @@ private void parseSerializedModule(String projectName, String forbiddenMessage) futures.add(future); return null; }); - futures.add(compiler.compile(false, true)); + futures.add(compiler.compile(false, true, true)); for (var f : futures) { var persisted = f.get(10, TimeUnit.SECONDS); assertEquals("Fib_Test library has been fully persisted", true, persisted); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerializationManagerTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerializationManagerTest.java index 24e0595f3653..e0dfa4f6b5d3 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerializationManagerTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/SerializationManagerTest.java @@ -89,7 +89,7 @@ public void serializeLibrarySuggestions() Object result = ensoContext .getCompiler() - .compile(false, false) + .compile(false, true, false) .get(COMPILE_TIMEOUT_SECONDS, TimeUnit.SECONDS); Assert.assertEquals(Boolean.TRUE, result); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/StaticAnalysisTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/StaticAnalysisTest.java new file mode 100644 index 000000000000..ae1e48489a28 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/StaticAnalysisTest.java @@ -0,0 +1,117 @@ +package org.enso.compiler.test; + +import com.oracle.truffle.api.TruffleFile; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import org.enso.common.LanguageInfo; +import org.enso.common.MethodNames; +import org.enso.common.RuntimeOptions; +import org.enso.compiler.core.ir.Module; +import org.enso.editions.LibraryName; +import org.enso.interpreter.runtime.EnsoContext; +import org.enso.interpreter.runtime.util.TruffleFileSystem; +import org.enso.interpreter.test.InterpreterContext; +import org.enso.pkg.Config; +import org.enso.pkg.Contact; +import org.enso.pkg.Package; +import org.enso.pkg.QualifiedName; +import org.graalvm.polyglot.Source; +import scala.Option; +import scala.jdk.javaapi.CollectionConverters; + +public abstract class StaticAnalysisTest { + + /** + * The interpreter context is needed here as it ensures initialization of everything needed to + * perform imports resolution, including PackageRepository. + * + *

Ideally, the tests for the static analysis capabilities of the compiler should _not_ depend + * on the Graal runtime context, as they should be runnable in other contexts - i.e. in a Visual + * Studio Code language server. + */ + private final InterpreterContext interpreterContext = + new InterpreterContext( + (builder) -> + builder + .option(RuntimeOptions.ENABLE_STATIC_ANALYSIS, "true") + .option(RuntimeOptions.LOG_LEVEL, Level.INFO.getName()) + .option(RuntimeOptions.LOG_LEVEL, Level.SEVERE.getName()) + .out(OutputStream.nullOutputStream()) + .err(OutputStream.nullOutputStream())); + + private final EnsoContext langCtx = + interpreterContext + .ctx() + .getBindings(LanguageInfo.ID) + .invokeMember(MethodNames.TopScope.LEAK_CONTEXT) + .asHostObject(); + + private final Map> syntheticTestPackages = new HashMap<>(); + + protected Module compile(Source src) { + String suffix = ".enso"; + String name = src.getName(); + if (!name.endsWith(suffix)) { + throw new IllegalArgumentException("Source name must end with " + suffix); + } + QualifiedName qualifiedName = + QualifiedName.fromString(name.substring(0, name.length() - suffix.length())); + + var packageRepository = langCtx.getPackageRepository(); + Package pkg = null; + + // If the module name is supposed to be put in a project, we register a synthetic project entry + // for it + if (qualifiedName.path().length() >= 2) { + var libraryName = + new LibraryName(qualifiedName.path().apply(0), qualifiedName.path().apply(1)); + if (!packageRepository.isPackageLoaded(libraryName)) { + // We are able only to register a synthetic package without associated Package<> object, but + // perhaps that's fine. + packageRepository.registerSyntheticPackage(libraryName.namespace(), libraryName.name()); + assert packageRepository.isPackageLoaded(libraryName); + } + + pkg = makeSyntheticPackageForTestProject(libraryName); + } + + // This creates the module and also registers it in the scope, so that import resolution will + // see it. + var module = + langCtx.getTopScope().createModule(qualifiedName, pkg, src.getCharacters().toString()); + langCtx.getCompiler().run(module.asCompilerModule()); + return module.getIr(); + } + + private Package makeSyntheticPackageForTestProject(LibraryName name) { + return syntheticTestPackages.computeIfAbsent( + name, + (unused) -> { + try { + var tmpRoot = Files.createTempDirectory("test-project-" + name); + TruffleFile root = langCtx.getPublicTruffleFile(tmpRoot.toString()); + List contacts = List.of(); + Config initialConfig = + new Config( + name.name(), + Option.empty(), + name.namespace(), + "0.0.0", + "", + CollectionConverters.asScala(contacts).toList(), + CollectionConverters.asScala(contacts).toList(), + Option.empty(), + true, + Option.empty()); + return new Package<>(root, initialConfig, TruffleFileSystem.INSTANCE); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypeInferenceTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypeInferenceTest.java index 33aa8b37b74f..31bac9136f8f 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypeInferenceTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypeInferenceTest.java @@ -1,44 +1,41 @@ package org.enso.compiler.test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import org.enso.compiler.Compiler; -import org.enso.compiler.Passes; -import org.enso.compiler.context.FreshNameSupply; -import org.enso.compiler.context.ModuleContext; +import org.enso.common.RuntimeOptions; import org.enso.compiler.core.IR; -import org.enso.compiler.core.ir.Diagnostic; -import org.enso.compiler.core.ir.Expression; import org.enso.compiler.core.ir.Module; import org.enso.compiler.core.ir.ProcessingPass; import org.enso.compiler.core.ir.Warning; import org.enso.compiler.core.ir.expression.Application; import org.enso.compiler.core.ir.module.scope.definition.Method; -import org.enso.compiler.data.CompilerConfig; -import org.enso.compiler.pass.PassConfiguration; -import org.enso.compiler.pass.PassManager; import org.enso.compiler.pass.analyse.types.InferredType; -import org.enso.compiler.pass.analyse.types.TypeInference; +import org.enso.compiler.pass.analyse.types.TypeInferencePropagation; import org.enso.compiler.pass.analyse.types.TypeRepresentation; -import org.enso.pkg.QualifiedName; +import org.enso.test.utils.ContextUtils; +import org.enso.test.utils.ModuleUtils; +import org.enso.test.utils.ProjectUtils; import org.graalvm.polyglot.Source; import org.junit.Ignore; import org.junit.Test; import scala.Option; -import scala.collection.immutable.Seq; -import scala.collection.immutable.Seq$; -import scala.jdk.javaapi.CollectionConverters; -public class TypeInferenceTest extends CompilerTests { - @Ignore("TODO resolving global methods") +public class TypeInferenceTest extends StaticAnalysisTest { @Test public void zeroAryCheck() throws Exception { final URI uri = new URI("memory://zeroAryModuleMethodCheck.enso"); @@ -60,12 +57,10 @@ public void zeroAryCheck() throws Exception { .buildLiteral(); Module module = compile(src); - Method foo = findStaticMethod(module, "foo"); - var x = findAssignment(foo.body(), "x"); - assertAtomType("zeroAryModuleMethodCheck.My_Type", x.expression()); + Method foo = ModuleUtils.findStaticMethod(module, "foo"); + assertAtomType("zeroAryModuleMethodCheck.My_Type", ModuleUtils.findAssignment(foo.body(), "x")); } - @Ignore("TODO resolution of global function application") @Test public void functionReturnCheck() throws Exception { final URI uri = new URI("memory://functionReturnCheck.enso"); @@ -88,12 +83,11 @@ public void functionReturnCheck() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); - var b = findAssignment(foo.body(), "b"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); String myType = "functionReturnCheck.My_Type"; // The result of `add a z` should be `My_Type` as guaranteed by the return type check of `add`. - assertAtomType(myType, b.expression()); + assertAtomType(myType, ModuleUtils.findAssignment(foo.body(), "b")); } @Test @@ -107,13 +101,13 @@ public void argChecks() throws Exception { Value v f1 (x1 : My_Type) = - y1 = x1 - My_Type.Value (y2.v + y2.v) + y1 = x1 + My_Type.Value (y1.v + y1.v) f2 : My_Type -> My_Type f2 x2 = - y2 = x2 - My_Type.Value (y2.v + y2.v) + y2 = x2 + My_Type.Value (y2.v + y2.v) f3 (x3 : My_Type) -> My_Type = My_Type.Value (x3.v + x3.v) """, @@ -124,17 +118,17 @@ public void argChecks() throws Exception { var module = compile(src); var myType = "argChecks.My_Type"; - var f1 = findStaticMethod(module, "f1"); - var f2 = findStaticMethod(module, "f2"); - var f3 = findStaticMethod(module, "f3"); + var f1 = ModuleUtils.findStaticMethod(module, "f1"); + var f2 = ModuleUtils.findStaticMethod(module, "f2"); + var f3 = ModuleUtils.findStaticMethod(module, "f3"); - assertAtomType(myType, findAssignment(f1, "y1")); - assertNoInferredType(findAssignment(f2, "y2")); + assertAtomType(myType, ModuleUtils.findAssignment(f1, "y1")); + assertNoInferredType(ModuleUtils.findAssignment(f2, "y2")); - assertEquals("(My_Type -> My_Type)", getInferredType(f1).toString()); + assertEquals("My_Type -> My_Type", getInferredType(f1).toString()); // f2 gets argument as Any, because the doc-signature is not checked - assertEquals("(Any -> My_Type)", getInferredType(f2).toString()); - assertEquals("(My_Type -> My_Type)", getInferredType(f3).toString()); + assertEquals("Any -> My_Type", getInferredType(f2).toString()); + assertEquals("My_Type -> My_Type", getInferredType(f3).toString()); } @Test @@ -156,10 +150,10 @@ public void ascribedExpressions() throws Exception { .buildLiteral(); Module module = compile(src); - Method f = findStaticMethod(module, "f"); + Method f = ModuleUtils.findStaticMethod(module, "f"); String myType = "ascribedExpressions.My_Type"; - assertAtomType(myType, findAssignment(f.body(), "y")); + assertAtomType(myType, ModuleUtils.findAssignment(f.body(), "y")); } @Test @@ -183,9 +177,9 @@ public void advancedAscribedExpressions() throws Exception { .buildLiteral(); Module module = compile(src); - Method f = findStaticMethod(module, "f"); + Method f = ModuleUtils.findStaticMethod(module, "f"); - var y1Type = getInferredType(findAssignment(f.body(), "y1")); + var y1Type = getInferredType(ModuleUtils.findAssignment(f.body(), "y1")); if (y1Type instanceof TypeRepresentation.SumType sumType) { var gotSet = new HashSet<>(sumType.types().stream().map(TypeRepresentation::toString).toList()); @@ -194,7 +188,7 @@ public void advancedAscribedExpressions() throws Exception { fail("y1 should be a sum type, but got " + y1Type); } - var y2Type = getInferredType(findAssignment(f.body(), "y2")); + var y2Type = getInferredType(ModuleUtils.findAssignment(f.body(), "y2")); if (y2Type instanceof TypeRepresentation.IntersectionType intersectionType) { var gotSet = new HashSet<>( @@ -226,7 +220,7 @@ public void ascribedFunctionType() throws Exception { .buildLiteral(); Module module = compile(src); - Method f = findStaticMethod(module, "f"); + Method f = ModuleUtils.findStaticMethod(module, "f"); // Here we will only know that both f1 and f2 are Any -> Any - because the ascribed check only // really performs a @@ -235,8 +229,10 @@ public void ascribedFunctionType() throws Exception { // unfortunately. TypeRepresentation primitiveFunctionType = new TypeRepresentation.ArrowType(TypeRepresentation.ANY, TypeRepresentation.ANY); - assertEquals(primitiveFunctionType, getInferredType(findAssignment(f.body(), "f1"))); - assertEquals(primitiveFunctionType, getInferredType(findAssignment(f.body(), "f2"))); + assertEquals( + primitiveFunctionType, getInferredType(ModuleUtils.findAssignment(f.body(), "f1"))); + assertEquals( + primitiveFunctionType, getInferredType(ModuleUtils.findAssignment(f.body(), "f2"))); } @Test @@ -258,12 +254,12 @@ public void literals() throws Exception { .buildLiteral(); Module module = compile(src); - Method f = findStaticMethod(module, "f"); + Method f = ModuleUtils.findStaticMethod(module, "f"); - assertAtomType("Standard.Base.Data.Numbers.Integer", findAssignment(f, "x")); - assertAtomType("Standard.Base.Data.Text.Text", findAssignment(f, "y")); - assertAtomType("Standard.Base.Data.Numbers.Float", findAssignment(f, "z")); - assertAtomType("Standard.Base.Data.Vector.Vector", findAssignment(f, "w")); + assertAtomType("Standard.Base.Data.Numbers.Integer", ModuleUtils.findAssignment(f, "x")); + assertAtomType("Standard.Base.Data.Text.Text", ModuleUtils.findAssignment(f, "y")); + assertAtomType("Standard.Base.Data.Numbers.Float", ModuleUtils.findAssignment(f, "z")); + assertAtomType("Standard.Base.Data.Vector.Vector", ModuleUtils.findAssignment(f, "w")); } @Test @@ -286,11 +282,11 @@ public void bindingsFlow() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "bindingsFlow.My_Type"; - assertAtomType(myType, findAssignment(foo, "w")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "w")); } @Test @@ -312,15 +308,15 @@ public void checkedArgumentTypes() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "checkedArgumentTypes.My_Type"; // Type from argument - assertAtomType(myType, findAssignment(foo, "y1")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "y1")); // No type - assertNoInferredType(findAssignment(foo, "y2")); + assertNoInferredType(ModuleUtils.findAssignment(foo, "y2")); } @Test @@ -344,13 +340,13 @@ public void innerFunctionType() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var f1Type = getInferredType(findAssignment(foo, "f1")); - assertEquals("(My_Type -> (My_Type -> My_Type))", f1Type.toString()); + var f1Type = getInferredType(ModuleUtils.findAssignment(foo, "f1")); + assertEquals("My_Type -> (My_Type -> My_Type)", f1Type.toString()); // and result of application is typed as the return type: - assertAtomType("innerFunctionType.My_Type", findAssignment(foo, "y")); + assertAtomType("innerFunctionType.My_Type", ModuleUtils.findAssignment(foo, "y")); } @Test @@ -372,10 +368,10 @@ public void zeroArgConstructor() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "zeroArgConstructor.My_Type"; - assertAtomType(myType, findAssignment(foo, "x")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x")); } @Test @@ -396,10 +392,40 @@ public void multiArgConstructor() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "multiArgConstructor.My_Type"; - assertAtomType(myType, findAssignment(foo, "x")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x")); + } + + @Test + public void nonexistentConstructor() throws Exception { + final URI uri = new URI("memory://nonexistentConstructor.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Value x y z + foo = + x = My_Type.Non_Existent 1 + x + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + var x = ModuleUtils.findAssignment(foo, "x"); + + assertNoInferredType(x); + assertEquals( + List.of( + new Warning.NoSuchMethod( + x.expression().identifiedLocation(), + "constructor `Non_Existent` on type (type My_Type)")), + ModuleUtils.getImmediateDiagnostics(x.expression())); } @Test @@ -427,7 +453,7 @@ public void constructorWithDefaults() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "constructorWithDefaults.My_Type"; @@ -436,21 +462,24 @@ public void constructorWithDefaults() throws Exception { // Before that is working, we just ensure we did not infer any 'unexpected' type for the // results. // assertAtomType(myType, findAssignment(foo, "x1")); - assertNoInferredType(findAssignment(foo, "x1")); + assertNoInferredType(ModuleUtils.findAssignment(foo, "x1")); // assertAtomType(myType, findAssignment(foo, "x2")); - assertNoInferredType(findAssignment(foo, "x2")); + assertNoInferredType(ModuleUtils.findAssignment(foo, "x2")); // assertAtomType(myType, findAssignment(foo, "x3")); - assertNoInferredType(findAssignment(foo, "x3")); + assertNoInferredType(ModuleUtils.findAssignment(foo, "x3")); - assertNotEquals(Optional.of(myType), getInferredTypeOption(findAssignment(foo, "x4"))); - assertNotEquals(Optional.of(myType), getInferredTypeOption(findAssignment(foo, "x5"))); + assertNotEquals( + Optional.of(myType), getInferredTypeOption(ModuleUtils.findAssignment(foo, "x4"))); + assertNotEquals( + Optional.of(myType), getInferredTypeOption(ModuleUtils.findAssignment(foo, "x5"))); // assertAtomType(myType, findAssignment(foo, "x6")); - assertNoInferredType(findAssignment(foo, "x6")); + assertNoInferredType(ModuleUtils.findAssignment(foo, "x6")); - assertNotEquals(Optional.of(myType), getInferredTypeOption(findAssignment(foo, "x7"))); + assertNotEquals( + Optional.of(myType), getInferredTypeOption(ModuleUtils.findAssignment(foo, "x7"))); } @Ignore("TODO: ifte") @@ -470,8 +499,8 @@ public void commonIfThenElse() throws Exception { .buildLiteral(); var module = compile(src); - var f = findStaticMethod(module, "f"); - assertAtomType("Standard.Base.Data.Numbers.Integer", findAssignment(f, "y")); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertAtomType("Standard.Base.Data.Numbers.Integer", ModuleUtils.findAssignment(f, "y")); } @Test @@ -496,8 +525,8 @@ public void commonCase() throws Exception { var module = compile(src); var myType = "commonCase.My_Type"; - var f = findStaticMethod(module, "f"); - assertAtomType(myType, findAssignment(f, "y")); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertAtomType(myType, ModuleUtils.findAssignment(f, "y")); } @Test @@ -521,8 +550,8 @@ public void inferBoundsFromCaseAlias() throws Exception { var module = compile(src); var myType = "inferBoundsFromCaseAlias.My_Type"; - var f = findStaticMethod(module, "f"); - assertAtomType(myType, findAssignment(f, "y")); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertAtomType(myType, ModuleUtils.findAssignment(f, "y")); } /** @@ -530,7 +559,7 @@ public void inferBoundsFromCaseAlias() throws Exception { * in one branch. We will need to ensure that we duplicate the local scopes in each branch to * avoid bad sharing. */ - @Ignore("TODO") + @Ignore("TODO for much much later: equality bounds in case") @Test public void inferEqualityBoundsFromCase() throws Exception { final URI uri = new URI("memory://inferEqualityBoundsFromCase.enso"); @@ -552,11 +581,11 @@ public void inferEqualityBoundsFromCase() throws Exception { var module = compile(src); var myType = "inferEqualityBoundsFromCase.My_Type"; - var f = findStaticMethod(module, "f"); - assertAtomType(myType, findAssignment(f, "y")); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertAtomType(myType, ModuleUtils.findAssignment(f, "y")); } - @Ignore("TODO") + @Ignore("TODO for much much later: equality bounds in case") @Test public void inferEqualityBoundsFromCaseLiteral() throws Exception { final URI uri = new URI("memory://inferEqualityBoundsFromCaseLiteral.enso"); @@ -575,11 +604,11 @@ public void inferEqualityBoundsFromCaseLiteral() throws Exception { .buildLiteral(); var module = compile(src); - var f = findStaticMethod(module, "f"); - assertSumType(findAssignment(f, "y"), "Integer", "Text"); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertSumType(ModuleUtils.findAssignment(f, "y"), "Integer", "Text"); } - @Ignore("TODO") + @Ignore("TODO for much much later: equality bounds in case") @Test public void inferEqualityBoundsFromCaseEdgeCase() throws Exception { // This test ensures that the equality bound from _:Other_Type is only applicable in its branch @@ -605,8 +634,8 @@ public void inferEqualityBoundsFromCaseEdgeCase() throws Exception { var module = compile(src); var myType = "inferEqualityBoundsFromCaseEdgeCase.My_Type"; - var f = findStaticMethod(module, "f"); - assertAtomType(myType, findAssignment(f, "y")); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertAtomType(myType, ModuleUtils.findAssignment(f, "y")); } @Test @@ -631,11 +660,11 @@ public void sumTypeFromCase() throws Exception { .buildLiteral(); var module = compile(src); - var f = findStaticMethod(module, "f"); - assertSumType(findAssignment(f, "y"), "My_Type", "Other_Type"); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertSumType(ModuleUtils.findAssignment(f, "y"), "My_Type", "Other_Type"); } - @Ignore + @Ignore("TODO: ifte") @Test public void sumTypeFromIf() throws Exception { final URI uri = new URI("memory://sumTypeFromIf.enso"); @@ -652,11 +681,11 @@ public void sumTypeFromIf() throws Exception { .buildLiteral(); var module = compile(src); - var f = findStaticMethod(module, "f"); - assertSumType(findAssignment(f, "y"), "Text", "Integer"); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertSumType(ModuleUtils.findAssignment(f, "y"), "Text", "Integer"); } - @Ignore + @Ignore("TODO: ifte") @Test public void sumTypeFromIfWithoutElse() throws Exception { final URI uri = new URI("memory://sumTypeFromIf.enso"); @@ -673,8 +702,8 @@ public void sumTypeFromIfWithoutElse() throws Exception { .buildLiteral(); var module = compile(src); - var f = findStaticMethod(module, "f"); - assertSumType(findAssignment(f, "y"), "Text", "Nothing"); + var f = ModuleUtils.findStaticMethod(module, "f"); + assertSumType(ModuleUtils.findAssignment(f, "y"), "Text", "Nothing"); } @Test @@ -706,18 +735,20 @@ member_method self (x : My_Type) = var module = compile(src); var myType = "typeInferenceWorksInsideMemberMethods.My_Type"; - var staticMethod = findMemberMethod(module, "My_Type", "static_method"); - assertAtomType(myType, findAssignment(staticMethod, "y")); - assertAtomType(myType, findAssignment(staticMethod, "z")); - assertAtomType("Standard.Base.Data.Numbers.Integer", findAssignment(staticMethod, "w")); - - var memberMethod = findMemberMethod(module, "My_Type", "member_method"); - assertAtomType(myType, findAssignment(memberMethod, "y")); - assertAtomType(myType, findAssignment(memberMethod, "z")); - assertAtomType("Standard.Base.Data.Numbers.Integer", findAssignment(memberMethod, "w")); + var staticMethod = ModuleUtils.findMemberMethod(module, "My_Type", "static_method"); + assertAtomType(myType, ModuleUtils.findAssignment(staticMethod, "y")); + assertAtomType(myType, ModuleUtils.findAssignment(staticMethod, "z")); + assertAtomType( + "Standard.Base.Data.Numbers.Integer", ModuleUtils.findAssignment(staticMethod, "w")); + + var memberMethod = ModuleUtils.findMemberMethod(module, "My_Type", "member_method"); + assertAtomType(myType, ModuleUtils.findAssignment(memberMethod, "y")); + assertAtomType(myType, ModuleUtils.findAssignment(memberMethod, "z")); + assertAtomType( + "Standard.Base.Data.Numbers.Integer", ModuleUtils.findAssignment(memberMethod, "w")); } - @Ignore("TODO") + @Ignore("TODO: self resolution") @Test public void typeInferenceOfSelf() throws Exception { final URI uri = new URI("memory://typeInferenceOfSelf.enso"); @@ -737,9 +768,9 @@ public void typeInferenceOfSelf() throws Exception { .buildLiteral(); var module = compile(src); - var f = findMemberMethod(module, "My_Type", "member_method"); + var f = ModuleUtils.findMemberMethod(module, "My_Type", "member_method"); var myType = "typeInferenceOfSelf.My_Type"; - assertAtomType(myType, findAssignment(f, "y")); + assertAtomType(myType, ModuleUtils.findAssignment(f, "y")); } @Test @@ -761,23 +792,23 @@ public void notInvokable() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var x1 = findAssignment(foo, "x1"); + var x1 = ModuleUtils.findAssignment(foo, "x1"); assertEquals( List.of(new Warning.NotInvokable(x1.expression().identifiedLocation(), "Integer")), - getImmediateDiagnostics(x1.expression())); + ModuleUtils.getImmediateDiagnostics(x1.expression())); - var x2 = findAssignment(foo, "x2"); + var x2 = ModuleUtils.findAssignment(foo, "x2"); assertEquals( List.of(new Warning.NotInvokable(x2.expression().identifiedLocation(), "Text")), - getImmediateDiagnostics(x2.expression())); + ModuleUtils.getImmediateDiagnostics(x2.expression())); - var x3 = findAssignment(foo, "x3"); + var x3 = ModuleUtils.findAssignment(foo, "x3"); assertEquals( "x3 should not contain any warnings", List.of(), - getDescendantsDiagnostics(x3.expression())); + ModuleUtils.getDescendantsDiagnostics(x3.expression())); } /** @@ -806,7 +837,7 @@ public void noErrorInParametricTypeSignatures() throws Exception { .buildLiteral(); var module = compile(src); - assertEquals(List.of(), getDescendantsDiagnostics(module)); + assertEquals(List.of(), ModuleUtils.getDescendantsDiagnostics(module)); } @Ignore("We cannot report type errors until we check there are no Conversions") @@ -831,12 +862,12 @@ public void typeErrorFromAscription() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var y = findAssignment(foo, "y"); + var y = ModuleUtils.findAssignment(foo, "y"); var typeError = new Warning.TypeMismatch(y.expression().identifiedLocation(), "Other_Type", "My_Type"); - assertEquals(List.of(typeError), getDescendantsDiagnostics(y.expression())); + assertEquals(List.of(typeError), ModuleUtils.getDescendantsDiagnostics(y.expression())); } @Test @@ -861,13 +892,13 @@ public void noTypeErrorIfConversionExists() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var y = findAssignment(foo, "y"); + var y = ModuleUtils.findAssignment(foo, "y"); assertEquals( "valid conversion should ensure there is no type error", List.of(), - getDescendantsDiagnostics(y.expression())); + ModuleUtils.getDescendantsDiagnostics(y.expression())); } @Ignore("We cannot report type errors until we check there are no Conversions") @@ -893,18 +924,18 @@ public void typeErrorFunctionToObject() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var y = findAssignment(foo, "y"); + var y = ModuleUtils.findAssignment(foo, "y"); var typeError1 = new Warning.TypeMismatch(y.expression().identifiedLocation(), "My_Type", "(Any -> Any)"); - assertEquals(List.of(typeError1), getDescendantsDiagnostics(y.expression())); + assertEquals(List.of(typeError1), ModuleUtils.getDescendantsDiagnostics(y.expression())); - var z = findAssignment(foo, "z"); + var z = ModuleUtils.findAssignment(foo, "z"); var typeError2 = new Warning.TypeMismatch( - z.expression().identifiedLocation(), "My_Type", "(My_Type -> My_Type)"); - assertEquals(List.of(typeError2), getDescendantsDiagnostics(z.expression())); + z.expression().identifiedLocation(), "My_Type", "My_Type -> My_Type"); + assertEquals(List.of(typeError2), ModuleUtils.getDescendantsDiagnostics(z.expression())); } @Ignore("We cannot report type errors until we check there are no Conversions") @@ -930,9 +961,9 @@ public void typeErrorInLocalCall() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var z = findAssignment(foo, "z"); + var z = ModuleUtils.findAssignment(foo, "z"); var arg = switch (z.expression()) { case Application.Prefix app -> app.arguments().head(); @@ -940,7 +971,7 @@ public void typeErrorInLocalCall() throws Exception { "Expected " + z.showCode() + " to be an application expression."); }; var typeError = new Warning.TypeMismatch(arg.identifiedLocation(), "Other_Type", "My_Type"); - assertEquals(List.of(typeError), getImmediateDiagnostics(arg)); + assertEquals(List.of(typeError), ModuleUtils.getImmediateDiagnostics(arg)); } @Ignore("We cannot report type errors until we check there are no Conversions") @@ -962,12 +993,12 @@ public void typeErrorInReturn() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var x = findAssignment(foo, "x"); + var x = ModuleUtils.findAssignment(foo, "x"); var typeError = new Warning.TypeMismatch(x.expression().identifiedLocation(), "My_Type", "Integer"); - assertEquals(List.of(typeError), getDescendantsDiagnostics(x.expression())); + assertEquals(List.of(typeError), ModuleUtils.getDescendantsDiagnostics(x.expression())); } @Test @@ -991,24 +1022,27 @@ public void noTypeErrorIfUnsure() throws Exception { .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); - var y = findAssignment(foo, "y"); + var y = ModuleUtils.findAssignment(foo, "y"); assertEquals( - "y should not contain any warnings", List.of(), getDescendantsDiagnostics(y.expression())); + "y should not contain any warnings", + List.of(), + ModuleUtils.getDescendantsDiagnostics(y.expression())); - var z = findAssignment(foo, "z"); + var z = ModuleUtils.findAssignment(foo, "z"); assertEquals( - "z should not contain any warnings", List.of(), getDescendantsDiagnostics(z.expression())); + "z should not contain any warnings", + List.of(), + ModuleUtils.getDescendantsDiagnostics(z.expression())); - var baz = findAssignment(foo, "baz"); + var baz = ModuleUtils.findAssignment(foo, "baz"); assertEquals( "baz should not contain any warnings", List.of(), - getDescendantsDiagnostics(baz.expression())); + ModuleUtils.getDescendantsDiagnostics(baz.expression())); } - @Ignore("TODO") @Test public void globalMethodTypes() throws Exception { final URI uri = new URI("memory://globalMethodTypes.enso"); @@ -1019,82 +1053,524 @@ public void globalMethodTypes() throws Exception { type My_Type Value v - lit = 42 - ctor = My_Type.Value 42 const -> My_Type = My_Type.Value 23 - check (x : My_Type) = x + check (x : My_Type) -> My_Type = x foo = - x1 = lit - x2 = ctor - x3 = const - x4 = check - x5 = check const - [x1, x2, x3, x4, x5] + x1 = const + x2 = check + x3 = check const + [x1, x2, x3] """, uri.getAuthority()) .uri(uri) .buildLiteral(); var module = compile(src); - var foo = findStaticMethod(module, "foo"); + var foo = ModuleUtils.findStaticMethod(module, "foo"); var myType = "globalMethodTypes.My_Type"; - assertAtomType("Standard.Base.Data.Numbers.Integer", findAssignment(foo, "x1")); - assertAtomType(myType, findAssignment(foo, "x2")); - assertAtomType(myType, findAssignment(foo, "x3")); - assertEquals("My_Type -> My_Type", getInferredType(findAssignment(foo, "x4")).toString()); - assertAtomType(myType, findAssignment(foo, "x5")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x1")); + assertEquals( + "My_Type -> My_Type", getInferredType(ModuleUtils.findAssignment(foo, "x2")).toString()); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x3")); } - private List getImmediateDiagnostics(IR ir) { - return CollectionConverters.asJava(ir.getDiagnostics().toList()); + @Test + public void memberMethodCalls() throws Exception { + final URI uri = new URI("memory://memberMethodCalls.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + + zero_arg self -> My_Type = My_Type.Value [self.v] + one_arg self (x : My_Type) -> My_Type = My_Type.Value [self.v, x.v] + + static_zero -> My_Type = My_Type.Value 42 + static_one (x : My_Type) -> My_Type = My_Type.Value [x.v, 1] + + My_Type.extension_method self -> My_Type = My_Type.Value [self.v, 2] + + foo = + inst = My_Type.Value 23 + x1 = inst.zero_arg + x2 = inst.one_arg inst + x3 = My_Type.static_zero + x4 = My_Type.static_one inst + + # And calling member methods through static syntax: + x5 = My_Type.zero_arg inst + x6 = My_Type.one_arg inst + + # And extension methods + x7 = inst.extension_method + x8 = My_Type.extension_method inst + [x1, x2, x3, x4, x5, x6, x7, x8] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + + var myType = "memberMethodCalls.My_Type"; + + assertAtomType(myType, ModuleUtils.findAssignment(foo, "inst")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x1")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x2")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x3")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x4")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x5")); + + // The function in x6 was not fully applied - still expecting 1 arg: + assertEquals( + "My_Type -> My_Type", getInferredType(ModuleUtils.findAssignment(foo, "x6")).toString()); + + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x7")); + assertAtomType(myType, ModuleUtils.findAssignment(foo, "x8")); } - private List getDescendantsDiagnostics(IR ir) { - return CollectionConverters.asJava( - ir.preorder().flatMap(node -> node.getDiagnostics().toList())); + @Ignore( + "TODO: error can only be reported when we can rule out there is no Other_Type -> My_Type" + + " conversion") + @Test + public void staticCallWithWrongType() throws Exception { + final URI uri = new URI("memory://staticCallWithWrongType.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + + member_method self = [self.v] + + type Other_Type + Constructor v + + member_method = [self.v, self.v] + + foo = + other = Other_Type.Constructor 44 + x1 = My_Type.member_method other + x1 + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + + var x1 = ModuleUtils.findAssignment(foo, "x1"); + var typeError = + new Warning.TypeMismatch(x1.expression().identifiedLocation(), "My_Type", "Other_Type"); + assertEquals(List.of(typeError), ModuleUtils.getDescendantsDiagnostics(x1.expression())); } - private Method findStaticMethod(Module module, String name) { - var option = - module - .bindings() - .find( - (def) -> - (def instanceof Method binding) - && binding.methodReference().typePointer().isEmpty() - && binding.methodReference().methodName().name().equals(name)); - - assertTrue("The method " + name + " should exist within the IR.", option.isDefined()); - return (Method) option.get(); + @Test + public void callingFieldGetters() throws Exception { + final URI uri = new URI("memory://callingFieldGetters.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Constructor_1 (field_a : Typ_X) (field_b : Typ_Y) + Constructor_2 (field_b : Typ_Z) + Constructor_3 (field_c : Typ_Z) + Constructor_4 (field_c : Typ_Z) + Constructor_5 field_d + + type Typ_X + type Typ_Y + type Typ_Z + + foo (instance : My_Type) = + x_a = instance.field_a + x_b = instance.field_b + x_c = instance.field_c + x_d = instance.field_d + [x_a, x_b, x_c, x_d] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + + assertAtomType("callingFieldGetters.Typ_X", ModuleUtils.findAssignment(foo, "x_a")); + // We don't know which constructor was used, so if the field appears in many constructors, it + // resolves to a sum type + assertSumType(ModuleUtils.findAssignment(foo, "x_b"), "Typ_Y", "Typ_Z"); + + // We have two constructors with field `field_c`, but they have the same type so the sum type + // should have been simplified + assertAtomType("callingFieldGetters.Typ_Z", ModuleUtils.findAssignment(foo, "x_c")); + + // We shouldn't get a No_Such_Method error on a field with no type ascription: + var x_d = ModuleUtils.findAssignment(foo, "x_d"); + assertEquals( + "Field access should not yield any warnings", + List.of(), + ModuleUtils.getDescendantsDiagnostics(x_d)); } - private Method findMemberMethod(Module module, String typeName, String name) { - var option = - module - .bindings() - .find( - (def) -> - (def instanceof Method binding) - && binding.methodReference().typePointer().isDefined() - && binding.methodReference().typePointer().get().name().equals(typeName) - && binding.methodReference().methodName().name().equals(name)); - - assertTrue("The method " + name + " should exist within the IR.", option.isDefined()); - return (Method) option.get(); + @Test + public void noSuchMethodStaticCheck() throws Exception { + final URI uri = new URI("memory://noSuchMethodStaticCheck.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + import Standard.Base.Any.Any + + type My_Type + Value v + + method_one self = 42 + static_method = 44 + + foo = + inst = My_Type.Value 23 + x1 = inst.method_one + x2 = inst.method_two + x3 = inst.to_text + x4 = inst.is_error + x5 = inst.static_method + [x1, x2, x3, x4, x5] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + var x1 = ModuleUtils.findAssignment(foo, "x1"); + var x2 = ModuleUtils.findAssignment(foo, "x2"); + var x3 = ModuleUtils.findAssignment(foo, "x3"); + var x4 = ModuleUtils.findAssignment(foo, "x4"); + var x5 = ModuleUtils.findAssignment(foo, "x5"); + + // member method is defined + assertEquals(List.of(), ModuleUtils.getDescendantsDiagnostics(x1.expression())); + + // this method is not found + assertEquals( + List.of( + new Warning.NoSuchMethod( + x2.expression().identifiedLocation(), + "member method `method_two` on type My_Type")), + ModuleUtils.getImmediateDiagnostics(x2.expression())); + + // delegating to Any + assertEquals(List.of(), ModuleUtils.getDescendantsDiagnostics(x3.expression())); + assertEquals(List.of(), ModuleUtils.getDescendantsDiagnostics(x4.expression())); + + // calling a static method on an instance _does not work_, so we get a warning telling there's + // no such _member_ method + assertEquals( + List.of( + new Warning.NoSuchMethod( + x5.expression().identifiedLocation(), + "member method `static_method` on type My_Type")), + ModuleUtils.getImmediateDiagnostics(x5.expression())); } - private Expression.Binding findAssignment(IR ir, String name) { - var option = - ir.preorder() - .find( - (node) -> - (node instanceof Expression.Binding binding) - && binding.name().name().equals(name)); - assertTrue("The binding `" + name + " = ...` should exist within the IR.", option.isDefined()); - return (Expression.Binding) option.get(); + @Test + public void alwaysKnowsMethodsOfAny() throws Exception { + final URI uri = new URI("memory://alwaysKnowsMethodsOfAny.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + + foo x = + txt1 = x.to_text + txt2 = 42.to_text + txt3 = (My_Type.Value 1).to_text + + bool = (x == x) + [txt1, txt2, txt3, bool] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + + assertAtomType("Standard.Base.Data.Text.Text", ModuleUtils.findAssignment(foo, "txt1")); + assertAtomType("Standard.Base.Data.Text.Text", ModuleUtils.findAssignment(foo, "txt2")); + assertAtomType("Standard.Base.Data.Text.Text", ModuleUtils.findAssignment(foo, "txt3")); + + assertAtomType("Standard.Base.Data.Boolean.Boolean", ModuleUtils.findAssignment(foo, "bool")); + } + + @Ignore("TODO: self resolution") + @Test + public void noSuchMethodOnSelf() throws Exception { + final URI uri = new URI("memory://noSuchMethodOnSelf.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + + method_one self = 42 + method_two self = + x1 = self.method_one + x2 = self.non_existent_method + [x1, x2] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var method_two = ModuleUtils.findMemberMethod(module, "My_Type", "method_two"); + var x1 = ModuleUtils.findAssignment(method_two, "x1"); + var x2 = ModuleUtils.findAssignment(method_two, "x2"); + + // member method is defined + assertEquals(List.of(), ModuleUtils.getDescendantsDiagnostics(x1.expression())); + + // this method is not found + assertEquals( + List.of( + new Warning.NoSuchMethod( + x2.expression().identifiedLocation(), + "member method `non_existent_method` on type My_Type")), + ModuleUtils.getImmediateDiagnostics(x2.expression())); + } + + @Test + public void callingExtensionMethodDefinedElsewhere() throws Exception { + final URI uriA = new URI("memory://local.Project1.modA.enso"); + final Source srcA = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + """, + uriA.getAuthority()) + .uri(uriA) + .buildLiteral(); + compile(srcA); + + final URI uriB = new URI("memory://local.Project1.modB.enso"); + final Source srcB = + Source.newBuilder( + "enso", + """ + import local.Project1.modA.My_Type + + type Typ_X + Value a + type Typ_Y + Value a + + My_Type.member self -> Typ_X = Typ_X.Value self + My_Type.static -> Typ_Y = Typ_Y.Value 32 + """, + uriB.getAuthority()) + .uri(uriB) + .buildLiteral(); + compile(srcB); + + final URI uriC = new URI("memory://local.Project1.modC.enso"); + final Source srcC = + Source.newBuilder( + "enso", + """ + import local.Project1.modA.My_Type + from local.Project1.modB import all + + foo = + inst = My_Type.Value 23 + x1 = inst.member + x2 = My_Type.static + [x1, x2] + """, + uriC.getAuthority()) + .uri(uriC) + .buildLiteral(); + var modC = compile(srcC); + var foo = ModuleUtils.findStaticMethod(modC, "foo"); + + assertAtomType("local.Project1.modB.Typ_X", ModuleUtils.findAssignment(foo, "x1")); + assertAtomType("local.Project1.modB.Typ_Y", ModuleUtils.findAssignment(foo, "x2")); + } + + @Test + public void callingReexportedExtensionMethods() throws Exception { + // Base type definition + final URI uriA = new URI("memory://local.Project1.modA.enso"); + final Source srcA = + Source.newBuilder( + "enso", + """ + type My_Type + Value v + """, + uriA.getAuthority()) + .uri(uriA) + .buildLiteral(); + compile(srcA); + + // Extension methods defined in another module + final URI uriB = new URI("memory://local.Project1.modB.enso"); + final Source srcB = + Source.newBuilder( + "enso", + """ + import local.Project1.modA.My_Type + + type Typ_X + Value a + type Typ_Y + Value a + + My_Type.member self -> Typ_X = Typ_X.Value self + My_Type.static -> Typ_Y = Typ_Y.Value 32 + """, + uriB.getAuthority()) + .uri(uriB) + .buildLiteral(); + compile(srcB); + + // Re-exports of the type and the extension method + final URI uriC = new URI("memory://local.Project1.modC.enso"); + final Source srcC = + Source.newBuilder( + "enso", + """ + export local.Project1.modA.My_Type + export local.Project1.modB.member + """, + uriC.getAuthority()) + .uri(uriC) + .buildLiteral(); + compile(srcC); + + final URI uriD = new URI("memory://local.Project1.modD.enso"); + final Source srcD = + Source.newBuilder( + "enso", + """ + from local.Project1.modC import all + + foo = + inst = My_Type.Value 23 + x1 = inst.member + x2 = My_Type.static + [x1, x2] + """, + uriD.getAuthority()) + .uri(uriD) + .buildLiteral(); + var modD = compile(srcD); + var foo = ModuleUtils.findStaticMethod(modD, "foo"); + + assertAtomType("local.Project1.modB.Typ_X", ModuleUtils.findAssignment(foo, "x1")); + assertAtomType("local.Project1.modB.Typ_Y", ModuleUtils.findAssignment(foo, "x2")); + } + + @Test + public void resolveImportedConstructor() throws Exception { + final URI uri = new URI("memory://local.Project1.modA.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + from project.modA.My_Type import My_Constructor + + type My_Type + My_Constructor v + + foo = + x1 = My_Constructor 1 + x1 + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + var x1 = ModuleUtils.findAssignment(foo, "x1"); + assertAtomType("local.Project1.modA.My_Type", x1); + } + + @Ignore("TODO: for later") + @Test + public void resolveFQNConstructor() throws Exception { + final URI uri = new URI("memory://local.Project1.modA.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type My_Type + My_Constructor v + + foo = + x1 = local.Project1.modA.My_Type.My_Constructor 1 + x1 + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var foo = ModuleUtils.findStaticMethod(module, "foo"); + var x1 = ModuleUtils.findAssignment(foo, "x1"); + assertAtomType("local.Project1.modA.My_Type", x1); + } + + @Test + public void staticTypeCheckerReportsWarningsOnProject() throws IOException { + var mainSrc = + """ + bar = + 1.non_existent_method + + main = + 42 + """; + Path projDir = Files.createTempDirectory("enso-tests"); + ProjectUtils.createProject("Proj", mainSrc, projDir); + var out = new ByteArrayOutputStream(); + var ctxBuilder = + ContextUtils.defaultContextBuilder() + .option(RuntimeOptions.DISABLE_IR_CACHES, "true") + .option(RuntimeOptions.ENABLE_STATIC_ANALYSIS, "true") + .option(RuntimeOptions.STRICT_ERRORS, "true") + .currentWorkingDirectory(projDir.getParent()) + .out(out) + .err(out) + .logHandler(out); + ProjectUtils.testProjectRun( + ctxBuilder, + projDir, + res -> { + assertThat(res.isNumber(), is(true)); + assertThat(res.asInt(), is(42)); + assertThat( + out.toString(), + containsString( + "Calling member method `non_existent_method` on type Integer will result in a" + + " No_Such_Method error")); + }); } private TypeRepresentation getInferredType(IR ir) { @@ -1106,7 +1582,7 @@ private TypeRepresentation getInferredType(IR ir) { } private Optional getInferredTypeOption(IR ir) { - Option metadata = ir.passData().get(TypeInference.INSTANCE); + Option metadata = ir.passData().get(TypeInferencePropagation.INSTANCE); if (metadata.isEmpty()) { return Optional.empty(); } else { @@ -1116,7 +1592,7 @@ private Optional getInferredTypeOption(IR ir) { } private void assertNoInferredType(IR ir) { - Option metadata = ir.passData().get(TypeInference.INSTANCE); + Option metadata = ir.passData().get(TypeInferencePropagation.INSTANCE); assertTrue( "Expecting " + ir.showCode() @@ -1126,11 +1602,24 @@ private void assertNoInferredType(IR ir) { } private void assertAtomType(String fqn, IR ir) { - var type = getInferredType(ir); + var option = getInferredTypeOption(ir); + if (option.isEmpty()) { + fail( + "Expected " + + ir.showCode() + + " to have Atom type " + + fqn + + ", but no type metadata was found."); + } + + var type = option.get(); if (type instanceof TypeRepresentation.AtomType atomType) { - assertEquals(fqn, atomType.fqn().toString()); + assertEquals( + "Expected " + ir.showCode() + " to have the right atom type: ", + fqn, + atomType.fqn().toString()); } else { - fail("Expected " + ir.showCode() + " to have an AtomType, but got " + type); + fail("Expected " + ir.showCode() + " to have an Atom type " + fqn + ", but got " + type); } } @@ -1144,48 +1633,4 @@ private void assertSumType(IR ir, String... shortNames) { fail("Expected " + ir.showCode() + " to have a SumType, but got " + type); } } - - /** - * Note that this `compile` method will not run import resolution. For now we just have tests that - * do not need it, and tests that do need it are placed in {@link - * org.enso.interpreter.test.TypeInferenceConsistencyTest} which spawns the whole interpreter. - * - *

If we want to run the imports resolution here, we need to create an instance of {@link - * Compiler}, like in {@link org.enso.compiler.test.semantic.TypeSignaturesTest}, but that relies - * on spawning a Graal context anyway. If possible I think it's good to skip that so that these - * tests can be kept simple - and the more complex ones can be done in the other suite. - */ - private Module compile(Source src) { - if (src.getCharacters().toString().contains("import")) { - throw new IllegalArgumentException("This method will not work correctly with imports."); - } - - Module rawModule = parse(src.getCharacters()); - - var compilerConfig = - new CompilerConfig(false, true, true, true, false, true, false, Option.empty()); - var passes = new Passes(compilerConfig); - @SuppressWarnings("unchecked") - var passConfig = - new PassConfiguration((Seq>) Seq$.MODULE$.empty()); - PassManager passManager = new PassManager(passes.passOrdering(), passConfig); - var compilerRunner = - new CompilerRunner() { - @Override - public CompilerConfig defaultConfig() { - return compilerConfig; - } - - @Override - public void org$enso$compiler$test$CompilerRunner$_setter_$defaultConfig_$eq( - CompilerConfig x$1) {} - }; - var moduleName = QualifiedName.simpleName(src.getName().replace(".enso", "")); - ModuleContext moduleContext = - compilerRunner.buildModuleContext( - moduleName, Option.apply(new FreshNameSupply()), Option.empty(), compilerConfig, false); - Module processedModule = - compilerRunner.runPassesOnModule(rawModule, passManager, moduleContext); - return processedModule; - } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypesFromSignaturesTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypesFromSignaturesTest.java new file mode 100644 index 000000000000..790cdd253776 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/TypesFromSignaturesTest.java @@ -0,0 +1,188 @@ +package org.enso.compiler.test; + +import static org.enso.test.utils.ModuleUtils.findMemberMethod; +import static org.enso.test.utils.ModuleUtils.findStaticMethod; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.net.URISyntaxException; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.ProcessingPass; +import org.enso.compiler.pass.analyse.types.InferredType; +import org.enso.compiler.pass.analyse.types.TypeInferenceSignatures; +import org.enso.compiler.pass.analyse.types.TypeRepresentation; +import org.graalvm.polyglot.Source; +import org.junit.Test; +import scala.Option; + +public class TypesFromSignaturesTest extends StaticAnalysisTest { + + @Test + public void simpleCheck() throws Exception { + final URI uri = new URI("memory://simpleCheck.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type A + type B + type C + Value v + + f (x : A) (y : B) -> C = C.Value [x, y] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var f1 = findStaticMethod(module, "f"); + assertTypeRepresentation(f1, "A -> (B -> C)"); + } + + @Test + public void variousExpressions() throws Exception { + final URI uri = new URI("memory://variousExpressions.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type A + Value + + self other = other + type B + + f1 (_ : A) -> B = 1 + f2 (x : A) -> B = x + 10 + f3 (x : A) -> B = [x] + f4 (x : A) -> B = f1 x + f5 (x : A) -> B = x + f6 (x : A) -> B = + y = x + z = y + z + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + assertTypeRepresentation(findStaticMethod(module, "f1"), "A -> B"); + assertTypeRepresentation(findStaticMethod(module, "f2"), "A -> B"); + assertTypeRepresentation(findStaticMethod(module, "f3"), "A -> B"); + assertTypeRepresentation(findStaticMethod(module, "f4"), "A -> B"); + assertTypeRepresentation(findStaticMethod(module, "f5"), "A -> B"); + assertTypeRepresentation(findStaticMethod(module, "f6"), "A -> B"); + } + + @Test + public void justArity() throws Exception { + final URI uri = new URI("memory://justArity.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type A + type B + type C + Value v + + f0 = 0 + + f4 x y z w = [x, y, z, w] + + f2 : A -> B -> C + f2 x y = [x, y] + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + + var f0 = findStaticMethod(module, "f0"); + var f4 = findStaticMethod(module, "f4"); + var f2 = findStaticMethod(module, "f2"); + + // For 0 arguments and unknown return type we know nothing useful, so no information is + // registered. + assertNoInferredType(f0); + + // For a function without ascriptions, we can at least infer the _arity_ + // Currently that is denoted by replacing unknowns with Any. Later this may be free type + // variables. + assertTypeRepresentation(f4, "Any -> (Any -> (Any -> (Any -> Any)))"); + + // For the 'opted-out' ascription, the types are ignored, because they are not checked types. + // But we still infer arity. + assertTypeRepresentation(f2, "Any -> (Any -> Any)"); + } + + @Test + public void memberMethods() throws URISyntaxException { + final URI uri = new URI("memory://memberMethods.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type A + static_method (x : A) -> A = x + member_method self (x : A) -> A = x + standalone_method (x : A) -> A = x + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var staticMethod = findMemberMethod(module, "A", "static_method"); + var memberMethod = findMemberMethod(module, "A", "member_method"); + + assertTypeRepresentation(staticMethod, "A -> A"); + assertTypeRepresentation(memberMethod, "A -> A"); + } + + @Test + public void extensionMethods() throws URISyntaxException { + final URI uri = new URI("memory://extensionMethods.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + type A + + A.extension_static_method (x : A) -> A = x + A.extension_member_method self (x : A) -> A = x + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = compile(src); + var staticMethod = findMemberMethod(module, "A", "extension_static_method"); + var memberMethod = findMemberMethod(module, "A", "extension_member_method"); + + assertTypeRepresentation(staticMethod, "A -> A"); + assertTypeRepresentation(memberMethod, "A -> A"); + } + + private void assertTypeRepresentation(IR ir, String expected) { + TypeRepresentation inferred = getInferredType(ir); + assertEquals(expected, inferred.toString()); + } + + private TypeRepresentation getInferredType(IR ir) { + Option metadata = ir.passData().get(TypeInferenceSignatures.INSTANCE); + assertTrue( + "Expecting " + ir.showCode() + " to contain a type within metadata.", metadata.isDefined()); + InferredType inferred = (InferredType) metadata.get(); + return inferred.type(); + } + + private void assertNoInferredType(IR ir) { + Option metadata = ir.passData().get(TypeInferenceSignatures.INSTANCE); + assertTrue( + "Expecting " + ir.showCode() + " to contain no type within metadata.", metadata.isEmpty()); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/SignatureTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/SignatureTest.java index 2c9da884f644..ab4f43705581 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/SignatureTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/SignatureTest.java @@ -1426,6 +1426,52 @@ public void returnTypeCheckProduct() throws Exception { } } + @Test + public void returnTypeCheckOptInErrorMethodsOnTypes() throws Exception { + final URI uri = new URI("memory://returnTypeCheckOptInErrorMethodsOnTypes.enso"); + final Source src = + Source.newBuilder( + "enso", + """ + from Standard.Base import Integer + type My_Type + Value + plus_member self a b -> Integer = b+a + plus_static a b -> Integer = b+a + """, + uri.getAuthority()) + .uri(uri) + .buildLiteral(); + + var module = ctx.eval(src); + + var res1 = + module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "My_Type.Value.plus_member 1 2"); + assertEquals(3, res1.asInt()); + + var res2 = module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "My_Type.plus_static 3 4"); + assertEquals(7, res2.asInt()); + + try { + var res = + module.invokeMember( + MethodNames.Module.EVAL_EXPRESSION, "My_Type.Value.plus_member 'a' 'b'"); + fail("Expecting an exception, not: " + res); + } catch (PolyglotException e) { + assertContains( + "expected the result of `plus_member` to be Integer, but got Text", e.getMessage()); + } + + try { + var res = + module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "My_Type.plus_static 'a' 'b'"); + fail("Expecting an exception, not: " + res); + } catch (PolyglotException e) { + assertContains( + "expected the result of `plus_static` to be Integer, but got Text", e.getMessage()); + } + } + static void assertTypeError(String expArg, String expType, String realType, String msg) { assertEquals( "Type error: expected " + expArg + " to be " + expType + ", but got " + realType + ".", diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TypeInferenceConsistencyTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TypeInferenceConsistencyTest.java index a48e6307b5bf..aed0895bd608 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TypeInferenceConsistencyTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TypeInferenceConsistencyTest.java @@ -272,7 +272,7 @@ private void assertNotInvokableRuntimeError(String got, PolyglotException except private static void assertContains(String exp, String msg) { if (!msg.contains(exp)) { - fail("Expecting " + msg + " to contain " + exp); + fail("Expecting '" + msg + "' to contain '" + exp + "'."); } } } diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/CompilerTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/CompilerTest.scala index a81def60f616..f82944c7f4af 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/CompilerTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/CompilerTest.scala @@ -323,7 +323,7 @@ trait CompilerRunner { compilerConfig = compilerConfig ) InlineContext( - module = mc, + moduleContext = mc, freshNameSupply = freshNameSupply, passConfiguration = passConfiguration, localScope = localScope, diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/analyse/BindingAnalysisTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/analyse/BindingAnalysisTest.scala index 8342d7245f75..439ebfb6bca4 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/analyse/BindingAnalysisTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/analyse/BindingAnalysisTest.scala @@ -18,7 +18,6 @@ import org.enso.compiler.data.BindingsMap.{ import org.enso.compiler.pass.analyse.BindingAnalysis import org.enso.compiler.pass.{PassConfiguration, PassGroup, PassManager} import org.enso.compiler.test.CompilerTest -import org.enso.persist.Persistance class BindingAnalysisTest extends CompilerTest { @@ -168,18 +167,15 @@ class BindingAnalysisTest extends CompilerTest { List( Argument( "a", - hasDefaultValue = false, - Persistance.Reference.none() + hasDefaultValue = false ), Argument( "b", - hasDefaultValue = false, - Persistance.Reference.none() + hasDefaultValue = false ), Argument( "c", - hasDefaultValue = false, - Persistance.Reference.none() + hasDefaultValue = false ) ), isProjectPrivate = false diff --git a/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/IrPersistance.java b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/IrPersistance.java index d317951014c2..4132ba57a683 100644 --- a/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/IrPersistance.java +++ b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/IrPersistance.java @@ -69,6 +69,8 @@ @Persistable(clazz = Warning.WrongBuiltinMethod.class, id = 789) @Persistable(clazz = Warning.NotInvokable.class, id = 791) @Persistable(clazz = Warning.TypeMismatch.class, id = 792) +@Persistable(clazz = Warning.NoSuchMethod.class, id = 793) +@Persistable(clazz = Warning.NonUnitTypeUsedOnValueLevel.class, id = 794) @Persistable(clazz = Operator.Binary.class, id = 790) public final class IrPersistance { private IrPersistance() {} diff --git a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Warning.scala b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Warning.scala index be4ff3e4ad10..c43986b46058 100644 --- a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Warning.scala +++ b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Warning.scala @@ -75,6 +75,24 @@ object Warning { override def diagnosticKeys(): Array[Any] = Array() } + /** A warning about calling a method (or field getter) that is not defined on the given type. + * + * This warning indicates a place that will result in a No_Such_Method error in runtime. + * + * @param identifiedLocation the location of the call + * @param methodDescription the description of the method + */ + case class NoSuchMethod( + override val identifiedLocation: IdentifiedLocation, + methodDescription: String + ) extends Warning { + override def message(source: (IdentifiedLocation => String)): String = { + s"Calling $methodDescription will result in a No_Such_Method error in runtime." + } + + override def diagnosticKeys(): Array[Any] = Array() + } + /** A warning about a `@Builtin_Method` annotation placed in a method * with unexpected body. * diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ImportExportScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ImportExportScope.java index 2d46bd8f8949..b61482281b61 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ImportExportScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ImportExportScope.java @@ -41,9 +41,9 @@ public Function getExportedMethod(Type type, String name) { } } - public Function getExportedConversion(Type type, Type target) { + public Function getExportedConversion(Type target, Type source) { if (isValidType(target)) { - return module.getScope().getExportedConversion(type, target); + return module.getScope().getExportedConversion(target, source); } else { return null; } @@ -57,13 +57,9 @@ public Function getMethodForType(Type type, String methodName) { } } - public Function getConversionForType(Type target, Type type) { + public Function getConversionForType(Type target, Type source) { if (isValidType(target)) { - var result = module.getScope().getConversionsFor(target); - if (result == null) { - return null; - } - return result.get(type); + return module.getScope().getConversionFor(target, source); } else { return null; } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java index 92d09901c40e..3d4efde52cd3 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java @@ -8,6 +8,7 @@ import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.enso.compiler.common.MethodResolutionAlgorithm; import org.enso.compiler.context.CompilerContext; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.Module; @@ -89,21 +90,78 @@ public Module getModule() { */ @CompilerDirectives.TruffleBoundary public Function lookupMethodDefinition(Type type, String name) { - var definedWithAtom = type.getDefinitionScope().getMethodForType(type, name); - if (definedWithAtom != null) { - return definedWithAtom; + return methodResolutionAlgorithm.lookupMethodDefinition(this, type, name); + } + + private final RuntimeMethodResolution methodResolutionAlgorithm = new RuntimeMethodResolution(); + + private static final class RuntimeMethodResolution + extends MethodResolutionAlgorithm { + + @Override + protected Collection getImportsFromModuleScope(ModuleScope moduleScope) { + return moduleScope.getImports(); + } + + @Override + protected Collection getExportsFromModuleScope(ModuleScope moduleScope) { + return moduleScope.getExports(); + } + + @Override + protected Function getConversionFromModuleScope( + ModuleScope moduleScope, Type target, Type source) { + return moduleScope.getConversionFor(target, source); + } + + @Override + protected Function getMethodFromModuleScope( + ModuleScope moduleScope, Type type, String methodName) { + return moduleScope.getMethodForType(type, methodName); + } + + @Override + protected ModuleScope findDefinitionScope(Type type) { + return type.getDefinitionScope(); } - var definedHere = getMethodForType(type, name); - if (definedHere != null) { - return definedHere; + @Override + protected Function getMethodForTypeFromScope( + ImportExportScope scope, Type type, String methodName) { + return scope.getMethodForType(type, methodName); } - return imports.stream() - .map(scope -> scope.getExportedMethod(type, name)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); + @Override + protected Function getExportedMethodFromScope( + ImportExportScope scope, Type type, String methodName) { + return scope.getExportedMethod(type, methodName); + } + + @Override + protected Function getConversionFromScope(ImportExportScope scope, Type target, Type source) { + return scope.getConversionForType(target, source); + } + + @Override + protected Function getExportedConversionFromScope( + ImportExportScope scope, Type target, Type source) { + return scope.getExportedConversion(target, source); + } + + @Override + protected Function onMultipleDefinitionsFromImports( + String methodName, List> methodFromImports) { + assert !methodFromImports.isEmpty(); + return methodFromImports.get(0).resolutionResult(); + } + } + + public Collection getImports() { + return imports; + } + + public Collection getExports() { + return exports; } /** @@ -119,51 +177,19 @@ public Function lookupMethodDefinition(Type type, String name) { * * @param source Source type * @param target Target type - * @return The conversion method or null if not found. + * @return The conversion method or null if not found.nie */ @CompilerDirectives.TruffleBoundary public Function lookupConversionDefinition(Type source, Type target) { - Function definedWithSource = source.getDefinitionScope().getConversionsFor(target).get(source); - if (definedWithSource != null) { - return definedWithSource; - } - Function definedWithTarget = target.getDefinitionScope().getConversionsFor(target).get(source); - if (definedWithTarget != null) { - return definedWithTarget; - } - Function definedHere = getConversionsFor(target).get(source); - if (definedHere != null) { - return definedHere; - } - return imports.stream() - .map(scope -> scope.getExportedConversion(source, target)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); + return methodResolutionAlgorithm.lookupConversionDefinition(this, source, target); } Function getExportedMethod(Type type, String name) { - var here = getMethodForType(type, name); - if (here != null) { - return here; - } - return exports.stream() - .map(scope -> scope.getMethodForType(type, name)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); + return methodResolutionAlgorithm.getExportedMethod(this, type, name); } - Function getExportedConversion(Type type, Type target) { - Function here = getConversionsFor(target).get(type); - if (here != null) { - return here; - } - return exports.stream() - .map(scope -> scope.getConversionForType(target, type)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); + Function getExportedConversion(Type target, Type source) { + return methodResolutionAlgorithm.getExportedConversion(this, target, source); } public List getAllTypes(String name) { @@ -232,12 +258,13 @@ public Set getMethodsForType(Type tpe) { } } - Map getConversionsFor(Type type) { - var result = conversions.get(type); - if (result == null) { - return new LinkedHashMap<>(); + public Function getConversionFor(Type target, Type source) { + var conversionsOnType = conversions.get(target); + if (conversionsOnType == null) { + return null; } - return result; + + return conversionsOnType.get(source); } /** @@ -506,6 +533,10 @@ public ModuleScope build() { return moduleScope; } + public Type getAssociatedType() { + return associatedType; + } + public static ModuleScope.Builder fromCompilerModuleScopeBuilder( CompilerContext.ModuleScopeBuilder scopeBuilder) { return ((TruffleCompilerModuleScopeBuilder) scopeBuilder).unsafeScopeBuilder(); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/TopLevelScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/TopLevelScope.java index d48cacddf02b..3c6aee846d8d 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/TopLevelScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/TopLevelScope.java @@ -184,10 +184,14 @@ private static Object leakContext(EnsoContext context) { @CompilerDirectives.TruffleBoundary private static Object compile(Object[] arguments, EnsoContext context) throws UnsupportedTypeException, ArityException { - boolean useGlobalCache = context.isUseGlobalCache(); boolean shouldCompileDependencies = Types.extractArguments(arguments, Boolean.class); + boolean shouldWriteCache = !context.isIrCachingDisabled(); + boolean useGlobalCache = context.isUseGlobalCache(); try { - return context.getCompiler().compile(shouldCompileDependencies, useGlobalCache).get(); + return context + .getCompiler() + .compile(shouldCompileDependencies, shouldWriteCache, useGlobalCache) + .get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala index 9790ed9c9be1..c932c2c63bd0 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala @@ -2,13 +2,13 @@ package org.enso.interpreter.runtime import com.oracle.truffle.api.source.{Source, SourceSection} import com.oracle.truffle.api.interop.InteropLibrary -import org.enso.compiler.pass.analyse.FramePointer -import org.enso.compiler.pass.analyse.FrameVariableNames -import org.enso.compiler.context.{ - CompilerContext, - LocalScope, +import org.enso.compiler.common.{ + BuildScopeFromModuleAlgorithm, NameResolutionAlgorithm } +import org.enso.compiler.pass.analyse.FramePointer +import org.enso.compiler.pass.analyse.FrameVariableNames +import org.enso.compiler.context.{CompilerContext, LocalScope} import org.enso.compiler.core.CompilerError import org.enso.compiler.core.ConstantsNames import org.enso.compiler.core.Implicits.AsMetadata @@ -28,9 +28,6 @@ import org.enso.compiler.core.ir.{ Type => Tpe } import org.enso.compiler.core.ir.module.scope.Definition -import org.enso.compiler.core.ir.module.scope.definition -import org.enso.compiler.core.ir.module.scope.Import -import org.enso.compiler.core.ir.module.scope.imports import org.enso.compiler.core.ir.expression.{ errors, Application, @@ -186,80 +183,226 @@ class IrToTruffle( * @param module the module for which code should be generated */ private def processModule(module: Module): Unit = { - generateReExportBindings(module) - val bindingsMap = - module - .unsafeGetMetadata( - BindingAnalysis, - "No binding analysis at the point of codegen." - ) - - registerModuleExports(bindingsMap) - registerModuleImports(bindingsMap) - registerPolyglotImports(module) + val bindingsMap = module.unsafeGetMetadata( + BindingAnalysis, + "No binding analysis at the point of codegen." + ) - registerTypeDefinitions(module) - registerMethodDefinitions(module) - registerConversions(module) + // TODO [RW] perhaps later this should be moved to the BuildModuleScopeFromModule + generateReExportBindings(module) + val builderAlgorithm = new BuildModuleScopeFromModule + builderAlgorithm.processModule(module, bindingsMap) scopeBuilder.build() } - private def registerModuleExports(bindingsMap: BindingsMap): Unit = - bindingsMap.getDirectlyExportedModules.foreach { exportedMod => - val exportedRuntimeMod = exportedMod.module.module.unsafeAsModule() - scopeBuilder.addExport( - new ImportExportScope(exportedRuntimeMod) - ) + final private class BuildModuleScopeFromModule + extends BuildScopeFromModuleAlgorithm[Type, ImportExportScope] { + + override protected def registerExport( + exportScope: ImportExportScope + ): Unit = { + scopeBuilder.addExport(exportScope) } - private def registerModuleImports(bindingsMap: BindingsMap): Unit = - bindingsMap.resolvedImports.foreach { imp => - imp.targets.foreach { - case _: BindingsMap.ResolvedType => - case _: BindingsMap.ResolvedConstructor => - case _: BindingsMap.ResolvedModuleMethod => - case _: BindingsMap.ResolvedExtensionMethod => - case _: BindingsMap.ResolvedConversionMethod => - case ResolvedModule(module) => - val mod = module - .unsafeAsModule() - val scope: ImportExportScope = imp.importDef.onlyNames - .map(only => new ImportExportScope(mod, only.map(_.name).asJava)) - .getOrElse(new ImportExportScope(mod)) - scopeBuilder.addImport(scope) + override protected def registerImport( + importScope: ImportExportScope + ): Unit = { + scopeBuilder.addImport(importScope) + } + + override protected def getTypeAssociatedWithCurrentScope(): Type = + scopeAssociatedType + + override protected def processPolyglotJavaImport( + visibleName: String, + javaClassName: String + ): Unit = + scopeBuilder.registerPolyglotSymbol( + visibleName, + () => context.lookupJavaClass(javaClassName) + ) + + override protected def processConversion( + conversion: Method.Conversion + ): Unit = { + def where() = + s"conversion `${conversion.typeName.map(_.name + ".").getOrElse("")}${conversion.methodName.name}`." + val scopeInfo = rootScopeInfo(where, conversion) + + def dataflowInfo() = conversion.unsafeGetMetadata( + DataflowAnalysis, + "Method definition missing dataflow information." + ) + def frameInfo() = conversion.unsafeGetMetadata( + FramePointerAnalysis, + "Method definition missing frame information." + ) + + val toOpt = + conversion.methodReference.typePointer match { + case Some(tpePointer) => + getTypeResolution(tpePointer) + case None => + Some(scopeAssociatedType) + } + val fromOpt = getTypeResolution(conversion.sourceTypeName) + toOpt.zip(fromOpt).foreach { case (toType, fromType) => + val expressionProcessor = new ExpressionProcessor( + toType.getName ++ Constants.SCOPE_SEPARATOR ++ conversion.methodName.name, + () => scopeInfo().graph, + () => scopeInfo().graph.rootScope, + dataflowInfo, + conversion.methodName.name, + frameInfo + ) + + val function = conversion.body match { + case fn: Function => + val bodyBuilder = + new expressionProcessor.BuildFunctionBody( + conversion.methodName.name, + fn.arguments, + fn.body, + TypeCheckValueNode.single("conversion", toType), + None, + true + ) + val rootNode = MethodRootNode.build( + language, + expressionProcessor.scope, + scopeBuilder.asModuleScope(), + () => bodyBuilder.bodyNode(), + makeSection(scopeBuilder.getModule, conversion.location), + toType, + conversion.methodName.name + ) + val callTarget = rootNode.getCallTarget + val arguments = bodyBuilder.args() + val funcSchema = FunctionSchema + .newBuilder() + .argumentDefinitions(arguments: _*) + .build() + new RuntimeFunction( + callTarget, + null, + funcSchema + ) + case _ => + throw new CompilerError( + s"Conversion bodies must be functions at the point of codegen (conversion $fromType to $toType)." + ) + } + scopeBuilder.registerConversionMethod(toType, fromType, function) } } - private def registerPolyglotImports(module: Module): Unit = - module.imports.foreach { - case poly @ imports.Polyglot(i: imports.Polyglot.Java, _, _, _) => - this.scopeBuilder.registerPolyglotSymbol( - poly.getVisibleName, + override protected def processMethodDefinition( + method: Method.Explicit + ): Unit = { + def where() = + s"`method ${method.typeName.map(_.name + ".").getOrElse("")}${method.methodName.name}`." + val scopeInfo = rootScopeInfo(where, method) + def dataflowInfo() = method.unsafeGetMetadata( + DataflowAnalysis, + "Method definition missing dataflow information." + ) + def frameInfo() = method.unsafeGetMetadata( + FramePointerAnalysis, + "Method definition missing frame information." + ) + + @tailrec + def getContext(tp: Expression): Option[String] = tp match { + case fn: Tpe.Function => getContext(fn.result) + case ctx: Tpe.Context => + ctx.context match { + case lit: Name.Literal => Some(lit.name) + case _ => None + } + case _ => None + } + + val effectContext = method + .getMetadata(TypeSignatures) + .flatMap(sig => getContext(sig.signature)) + + val cons = getTypeDefiningMethod(method) + if (cons != null) { + val fullMethodDefName = + cons.getName ++ Constants.SCOPE_SEPARATOR ++ method.methodName.name + val expressionProcessor = new ExpressionProcessor( + fullMethodDefName, + () => scopeInfo().graph, + () => scopeInfo().graph.rootScope, + dataflowInfo, + fullMethodDefName, + frameInfo + ) + + scopeBuilder.registerMethod( + cons, + method.methodName.name, () => { - val hostSymbol = context.lookupJavaClass(i.getJavaName) - hostSymbol + buildFunction( + method, + effectContext, + cons, + fullMethodDefName, + expressionProcessor + ) } ) - case _: Import.Module => - case _: Error => + } } - private def registerTypeDefinitions(module: Module): Unit = { - val typeDefs = module.bindings.collect { case tp: Definition.Type => tp } - typeDefs.foreach { tpDef => - // Register the atoms and their constructors in scope - val atomDefs = tpDef.members - val asType = scopeBuilder.asModuleScope().getType(tpDef.name.name, true) + override protected def processTypeDefinition(typ: Definition.Type): Unit = { + val atomDefs = typ.members + val asType = + scopeBuilder.asModuleScope().getType(typ.name.name, true) val atomConstructors = atomDefs.map(cons => asType.getConstructors.get(cons.name.name)) atomConstructors .zip(atomDefs) .foreach { case (atomCons, atomDefn) => - registerAtomConstructor(tpDef, atomCons, atomDefn) + registerAtomConstructor(typ, atomCons, atomDefn) } asType.generateGetters(language) } + + override protected def associatedTypeFromResolvedModule( + module: ResolvedModule + ): Type = + asAssociatedType(module.module.unsafeAsModule()) + + override protected def associatedTypeFromResolvedType( + `type`: BindingsMap.ResolvedType, + isStatic: Boolean + ): Type = { + val associatedType = asType(`type`) + if (isStatic) { + associatedType.getEigentype + } else { + associatedType + } + } + + override protected def buildExportScope( + exportedModule: BindingsMap.ExportedModule + ): ImportExportScope = { + val exportedRuntimeMod = exportedModule.module.module.unsafeAsModule() + new ImportExportScope(exportedRuntimeMod) + } + + override protected def buildImportScope( + resolvedImport: BindingsMap.ResolvedImport, + resolvedModule: ResolvedModule + ): ImportExportScope = { + val mod = resolvedModule.module.unsafeAsModule() + resolvedImport.importDef.onlyNames + .map(only => new ImportExportScope(mod, only.map(_.name).asJava)) + .getOrElse(new ImportExportScope(mod)) + } } private def registerAtomConstructor( @@ -270,7 +413,7 @@ class IrToTruffle( val initializationBuilderSupplier : Supplier[AtomConstructor.InitializationBuilder] = () => { - val scopeInfo = rootScopeInfo("atom definition", atomDefn) + val scopeInfo = rootScopeInfo(() => "atom definition", atomDefn) def dataflowInfo() = atomDefn.unsafeGetMetadata( DataflowAnalysis, @@ -376,77 +519,6 @@ class IrToTruffle( } } - private def registerMethodDefinitions(module: Module): Unit = { - val methodDefs = module.bindings.collect { - case method: definition.Method.Explicit => method - } - - methodDefs.foreach(methodDef => { - lazy val where = - s"`method ${methodDef.typeName.map(_.name + ".").getOrElse("")}${methodDef.methodName.name}`." - val scopeInfo = rootScopeInfo(where, methodDef) - def dataflowInfo() = methodDef.unsafeGetMetadata( - DataflowAnalysis, - "Method definition missing dataflow information." - ) - def frameInfo() = methodDef.unsafeGetMetadata( - FramePointerAnalysis, - "Method definition missing frame information." - ) - - @tailrec - def getContext(tp: Expression): Option[String] = tp match { - case fn: Tpe.Function => getContext(fn.result) - case ctx: Tpe.Context => - ctx.context match { - case lit: Name.Literal => Some(lit.name) - case _ => None - } - case _ => None - } - - val effectContext = methodDef - .getMetadata(TypeSignatures) - .flatMap(sig => getContext(sig.signature)) - - val declaredConsOpt = - getTypeAssociatedWithMethodDefinition(methodDef) - - val consOpt = declaredConsOpt.map { c => - if (methodDef.isStatic) { - c.getEigentype - } else { c } - } - - consOpt.foreach { cons => - val fullMethodDefName = - cons.getName ++ Constants.SCOPE_SEPARATOR ++ methodDef.methodName.name - val expressionProcessor = new ExpressionProcessor( - fullMethodDefName, - () => scopeInfo().graph, - () => scopeInfo().graph.rootScope, - dataflowInfo, - fullMethodDefName, - frameInfo - ) - - scopeBuilder.registerMethod( - cons, - methodDef.methodName.name, - () => { - buildFunction( - methodDef, - effectContext, - cons, - fullMethodDefName, - expressionProcessor - ) - } - ) - } - }) - } - private def buildFunction( methodDef: Method.Explicit, effectContext: Option[String], @@ -559,7 +631,7 @@ class IrToTruffle( val scopeName = scopeElements.mkString(Constants.SCOPE_SEPARATOR) - lazy val where = + def where() = s"annotation ${annotation.name} of method ${scopeElements.init .mkString(Constants.SCOPE_SEPARATOR)}" val scopeInfo = rootScopeInfo(where, annotation) @@ -727,133 +799,6 @@ class IrToTruffle( ) } - private def getTypeAssociatedWithMethodDefinition( - methodDef: Method.Explicit - ): Option[Type] = { - methodDef.methodReference.typePointer match { - case None => - Some(scopeAssociatedType) - case Some(tpePointer) => - tpePointer - .getMetadata( - MethodDefinitions.INSTANCE, - classOf[BindingsMap.Resolution] - ) - .map { res => - res.target match { - case binding @ BindingsMap.ResolvedType(_, _) => - asType(binding) - case BindingsMap.ResolvedModule(module) => - asAssociatedType(module.unsafeAsModule()) - case BindingsMap.ResolvedConstructor(_, _) => - throw new CompilerError( - "Impossible, should be caught by MethodDefinitions pass" - ) - case BindingsMap.ResolvedPolyglotSymbol(_, _) => - throw new CompilerError( - "Impossible polyglot symbol, should be caught by MethodDefinitions pass." - ) - case BindingsMap.ResolvedPolyglotField(_, _) => - throw new CompilerError( - "Impossible polyglot field, should be caught by MethodDefinitions pass." - ) - case _: BindingsMap.ResolvedModuleMethod => - throw new CompilerError( - "Impossible module method here, should be caught by MethodDefinitions pass." - ) - case _: BindingsMap.ResolvedExtensionMethod => - throw new CompilerError( - "Impossible static method here, should be caught by MethodDefinitions pass." - ) - case _: BindingsMap.ResolvedConversionMethod => - throw new CompilerError( - "Impossible conversion method here, should be caught by MethodDefinitions pass." - ) - } - } - } - } - - private def registerConversions(module: Module): Unit = { - val conversionDefs = module.bindings.collect { - case conversion: definition.Method.Conversion => - conversion - } - - // Register the conversion definitions in scope - conversionDefs.foreach(methodDef => { - lazy val where = - s"conversion `${methodDef.typeName.map(_.name + ".").getOrElse("")}${methodDef.methodName.name}`." - val scopeInfo = rootScopeInfo(where, methodDef) - - def dataflowInfo() = methodDef.unsafeGetMetadata( - DataflowAnalysis, - "Method definition missing dataflow information." - ) - def frameInfo() = methodDef.unsafeGetMetadata( - FramePointerAnalysis, - "Method definition missing frame information." - ) - - val toOpt = - methodDef.methodReference.typePointer match { - case Some(tpePointer) => - getTypeResolution(tpePointer) - case None => - Some(scopeAssociatedType) - } - val fromOpt = getTypeResolution(methodDef.sourceTypeName) - toOpt.zip(fromOpt).foreach { case (toType, fromType) => - val expressionProcessor = new ExpressionProcessor( - toType.getName ++ Constants.SCOPE_SEPARATOR ++ methodDef.methodName.name, - () => scopeInfo().graph, - () => scopeInfo().graph.rootScope, - dataflowInfo, - methodDef.methodName.name, - frameInfo - ) - - val function = methodDef.body match { - case fn: Function => - val bodyBuilder = - new expressionProcessor.BuildFunctionBody( - methodDef.methodName.name, - fn.arguments, - fn.body, - TypeCheckValueNode.single("conversion", toType), - None, - true - ) - val rootNode = MethodRootNode.build( - language, - expressionProcessor.scope, - scopeBuilder.asModuleScope(), - () => bodyBuilder.bodyNode(), - makeSection(scopeBuilder.getModule, methodDef.location), - toType, - methodDef.methodName.name - ) - val callTarget = rootNode.getCallTarget - val arguments = bodyBuilder.args() - val funcSchema = FunctionSchema - .newBuilder() - .argumentDefinitions(arguments: _*) - .build() - new RuntimeFunction( - callTarget, - null, - funcSchema - ) - case _ => - throw new CompilerError( - "Conversion bodies must be functions at the point of codegen." - ) - } - scopeBuilder.registerConversionMethod(toType, fromType, function) - } - }) - } - // ========================================================================== // === Utility Functions ==================================================== // ========================================================================== @@ -2003,7 +1948,7 @@ class IrToTruffle( setLocation(nameExpr, name.location) } - private class RuntimeNameResolution + final private class RuntimeNameResolution extends NameResolutionAlgorithm[ RuntimeExpression, FramePointer, @@ -2027,7 +1972,8 @@ class IrToTruffle( ReadLocalVariableNode.build(localLink) override protected def resolveGlobalName( - resolvedName: BindingsMap.ResolvedName + resolvedName: BindingsMap.ResolvedName, + relatedIr: IR ): RuntimeExpression = nodeForResolution(resolvedName) @@ -2671,10 +2617,10 @@ class IrToTruffle( } private def scopeAssociatedType = - scopeBuilder.asModuleScope().getAssociatedType + scopeBuilder.getAssociatedType private def rootScopeInfo( - where: => String, + where: () => String, ir: IR ): () => AliasMetadata.RootScope = { def readScopeInfo() = { diff --git a/engine/runtime/src/test/java/org/enso/runtime/test/TypeMetadataPersistanceTest.java b/engine/runtime/src/test/java/org/enso/runtime/test/TypeMetadataPersistanceTest.java index 4665bfdfc5d7..4ec8edf7a57b 100644 --- a/engine/runtime/src/test/java/org/enso/runtime/test/TypeMetadataPersistanceTest.java +++ b/engine/runtime/src/test/java/org/enso/runtime/test/TypeMetadataPersistanceTest.java @@ -4,24 +4,18 @@ import java.io.IOException; import java.util.List; -import org.enso.compiler.data.BindingsMap; -import org.enso.compiler.pass.analyse.types.AtomTypeInterfaceFromBindingsMap; import org.enso.compiler.pass.analyse.types.InferredType; import org.enso.compiler.pass.analyse.types.TypeRepresentation; import org.enso.persist.Persistance; import org.enso.pkg.QualifiedName; +import org.enso.scala.wrapper.ScalaConversions; import org.junit.Test; -import scala.jdk.javaapi.CollectionConverters$; /** * Currently the static type inference pass is optional and it is not computed as part of the cache * indexing. */ public class TypeMetadataPersistanceTest { - private static scala.collection.immutable.List makeScalaList(List list) { - return CollectionConverters$.MODULE$.asScala(list).toList(); - } - private static T serde(Class clazz, T l) throws IOException { var arr = Persistance.write(l, null); var ref = Persistance.read(arr, null); @@ -41,17 +35,7 @@ public void writeSomeInferredType() throws Exception { } private TypeRepresentation.TypeObject mockObject() { - var fqn = new QualifiedName(makeScalaList(List.of("mod")), "Test"); - return new TypeRepresentation.TypeObject(fqn, mockAtomType()); - } - - private AtomTypeInterfaceFromBindingsMap mockAtomType() { - scala.collection.immutable.List params = makeScalaList(List.of()); - var ctorArgs = - makeScalaList( - List.of(new BindingsMap.Argument("arg", false, Persistance.Reference.none()))); - var constructors = makeScalaList(List.of(new BindingsMap.Cons("ctor", ctorArgs, false))); - return new AtomTypeInterfaceFromBindingsMap( - new BindingsMap.Type("Test", params, constructors, false)); + var fqn = new QualifiedName(ScalaConversions.seq(List.of("mod")).toList(), "Test"); + return new TypeRepresentation.TypeObject(fqn); } } diff --git a/lib/java/persistance/src/main/java/org/enso/persist/PerMap.java b/lib/java/persistance/src/main/java/org/enso/persist/PerMap.java index 0bc1445b72de..084a7748e031 100644 --- a/lib/java/persistance/src/main/java/org/enso/persist/PerMap.java +++ b/lib/java/persistance/src/main/java/org/enso/persist/PerMap.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.openide.util.lookup.Lookups; final class PerMap { @@ -34,7 +35,7 @@ private PerMap() { throw new IllegalStateException( "Multiple registrations for ID " + p.id + " " + prevId + " != " + p); } - hash += p.id; + hash = Objects.hash(hash, p.id); var prevType = types.put(p.clazz, p); if (prevType != null) { throw new IllegalStateException( diff --git a/lib/java/test-utils/src/main/java/org/enso/test/utils/ModuleUtils.java b/lib/java/test-utils/src/main/java/org/enso/test/utils/ModuleUtils.java index e715f111a08f..956f5b834354 100644 --- a/lib/java/test-utils/src/main/java/org/enso/test/utils/ModuleUtils.java +++ b/lib/java/test-utils/src/main/java/org/enso/test/utils/ModuleUtils.java @@ -4,6 +4,11 @@ import java.util.List; import java.util.Map; import org.enso.compiler.context.CompilerContext.Module; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.Diagnostic; +import org.enso.compiler.core.ir.DiagnosticStorage; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.module.scope.definition.Method; import org.enso.compiler.data.BindingsMap.DefinedEntity; import org.enso.compiler.data.BindingsMap.ResolvedImport; import org.enso.compiler.data.BindingsMap.ResolvedName; @@ -68,4 +73,70 @@ private static Map> getExportedSymbols(Module module) }); return bindings; } + + public static List getImmediateDiagnostics(IR ir) { + return CollectionConverters.asJava(ir.getDiagnostics().toList()); + } + + public static List getDescendantsDiagnostics(IR ir) { + return CollectionConverters.asJava( + ir.preorder() + .flatMap( + (node) -> { + DiagnosticStorage diagnostics = node.getDiagnostics(); + if (diagnostics != null) { + return diagnostics.toList(); + } else { + return scala.collection.immutable.List$.MODULE$.empty(); + } + })); + } + + public static Method findStaticMethod(org.enso.compiler.core.ir.Module module, String name) { + var option = + module + .bindings() + .find( + (def) -> + (def instanceof Method binding) + && binding.methodReference().typePointer().isEmpty() + && binding.methodReference().methodName().name().equals(name)); + + if (option.isEmpty()) { + throw new IllegalStateException("The method " + name + " should exist within the IR."); + } + return (Method) option.get(); + } + + public static Method findMemberMethod( + org.enso.compiler.core.ir.Module module, String typeName, String name) { + var option = + module + .bindings() + .find( + (def) -> + (def instanceof Method binding) + && binding.methodReference().typePointer().isDefined() + && binding.methodReference().typePointer().get().name().equals(typeName) + && binding.methodReference().methodName().name().equals(name)); + + if (option.isEmpty()) { + throw new IllegalStateException("The method " + name + " should exist within the IR."); + } + return (Method) option.get(); + } + + public static Expression.Binding findAssignment(IR ir, String name) { + var option = + ir.preorder() + .find( + (node) -> + (node instanceof Expression.Binding binding) + && binding.name().name().equals(name)); + if (option.isEmpty()) { + throw new IllegalStateException( + "The binding `" + name + " = ...` should exist within the IR."); + } + return (Expression.Binding) option.get(); + } } diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala index 3adb2ecfd257..49fde351125c 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala @@ -96,9 +96,6 @@ object Contact { * edition * @param componentGroups the description of component groups provided by this * package - * @param originalJson a Json object holding the original values that this - * Config was created from, used to preserve configuration - * keys that are not known */ case class Config( name: String, diff --git a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso index 832212d22f84..e74970f4350d 100644 --- a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso +++ b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso @@ -15,3 +15,6 @@ type Any @Builtin_Type type Default_Comparator + + ## PRIVATE + less_than_builtin left right = @Builtin_Method "Default_Comparator.less_than_builtin" diff --git a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso index b5b066c3832b..916ca2f6d055 100644 --- a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso +++ b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso @@ -32,4 +32,6 @@ type Project_Description name self = self.prim_config.name + enso_project_builtin module = @Builtin_Method "Project_Description.enso_project_builtin" + enso_project = Project_Description.enso_project_builtin Nothing From 25933afeec1d942598bd43f2765ce134bd5332eb Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Thu, 9 Jan 2025 02:36:31 +1000 Subject: [PATCH 3/9] Batch asset invalidations (#11937) - Close https://github.com/enso-org/cloud-v2/issues/1627 - Batch asset invalidations for: - Bulk deletes - Bulk undo deletes (restores) - Bulk copy/move - Bulk download - This avoids flickering when the directory list is invalidated multiple times (once for the mutation corresponding to each asset) Codebase changes: - Remove all `AssetEvent`s and `AssetListEvent`s. Remaining events have been moved to TanStack Query mutations in this PR, as it is neccessary for batch invalidation functionality. - Remove `key` and `directoryKey` from `AssetTreeNode`, and `key`s in general in favor of `id`s. Not *strictly* necessary, but it was causing logic issues and is (IMO) a nice simplification to be able to do. # Important Notes None --- .gitignore | 6 + app/common/src/backendQuery.ts | 10 +- app/common/src/queryClient.ts | 4 + app/common/src/services/Backend.ts | 22 +- app/common/src/text/english.json | 4 - app/common/src/utilities/data/array.ts | 2 +- app/common/src/utilities/data/object.ts | 9 + .../integration-test/dashboard/delete.spec.ts | 7 - .../dashboard/labelsPanel.spec.ts | 3 +- app/gui/src/dashboard/App.tsx | 5 +- .../components/dashboard/AssetRow.tsx | 390 ++--- .../dashboard/components/dashboard/Label.tsx | 2 +- .../components/dashboard/ProjectIcon.tsx | 1 - .../dashboard/components/dashboard/column.ts | 7 +- .../dashboard/column/LabelsColumn.tsx | 52 +- .../dashboard/column/PathColumn.tsx | 31 +- .../dashboard/column/SharedWithColumn.tsx | 7 +- .../src/dashboard/events/AssetEventType.ts | 22 - .../dashboard/events/AssetListEventType.ts | 13 - app/gui/src/dashboard/events/assetEvent.ts | 101 -- .../src/dashboard/events/assetListEvent.ts | 133 -- .../dashboard/hooks/backendBatchedHooks.ts | 303 ++++ app/gui/src/dashboard/hooks/backendHooks.ts | 913 ++++++++++++ app/gui/src/dashboard/hooks/backendHooks.tsx | 1290 ----------------- .../hooks/backendUploadFilesHooks.tsx | 530 +++++++ .../src/dashboard/hooks/cutAndPasteHooks.ts | 50 + app/gui/src/dashboard/hooks/projectHooks.ts | 5 +- .../dashboard/layouts/AssetContextMenu.tsx | 71 +- .../src/dashboard/layouts/AssetProperties.tsx | 5 +- .../src/dashboard/layouts/AssetSearchBar.tsx | 8 +- .../layouts/AssetVersions/AssetVersion.tsx | 37 +- app/gui/src/dashboard/layouts/AssetsTable.tsx | 714 +++------ .../layouts/AssetsTableContextMenu.tsx | 98 +- .../dashboard/layouts/CategorySwitcher.tsx | 18 +- app/gui/src/dashboard/layouts/Drive.tsx | 18 +- .../layouts/Drive/Categories/Category.ts | 63 +- .../layouts/Drive/EventListProvider.tsx | 190 --- .../layouts/Drive/assetTreeHooks.tsx | 147 +- .../layouts/Drive/assetsTableItemsHooks.tsx | 14 +- .../layouts/Drive/directoryIdsHooks.tsx | 13 +- .../layouts/Drive/fetchDirectoriesHooks.ts | 34 + app/gui/src/dashboard/layouts/DriveBar.tsx | 52 +- app/gui/src/dashboard/layouts/Editor.tsx | 5 - .../dashboard/layouts/GlobalContextMenu.tsx | 13 +- app/gui/src/dashboard/layouts/Labels.tsx | 29 +- .../Settings/ActivityLogSettingsSection.tsx | 4 +- .../KeyboardShortcutsSettingsSection.tsx | 3 +- .../layouts/Settings/ProfilePictureInput.tsx | 2 - .../layouts/Settings/UserGroupRow.tsx | 10 +- .../layouts/Settings/UserGroupUserRow.tsx | 13 +- .../Settings/UserGroupsSettingsSection.tsx | 2 +- .../dashboard/layouts/Settings/UserRow.tsx | 10 +- app/gui/src/dashboard/layouts/TabBar.tsx | 13 +- .../dashboard/modals/ConfirmDeleteModal.tsx | 6 +- .../modals/ConfirmDeleteUserModal.tsx | 4 - .../dashboard/modals/ManageLabelsModal.tsx | 12 +- .../src/dashboard/modals/NewLabelModal.tsx | 14 +- .../dashboard/modals/NewUserGroupModal.tsx | 17 +- .../dashboard/pages/dashboard/Dashboard.tsx | 30 +- .../src/dashboard/providers/DriveProvider.tsx | 138 +- .../src/dashboard/services/LocalBackend.ts | 64 +- .../src/dashboard/services/ProjectManager.ts | 20 +- .../src/dashboard/services/RemoteBackend.ts | 40 +- .../dashboard/services/remoteBackendPaths.ts | 9 - .../src/dashboard/utilities/AssetTreeNode.ts | 51 +- 65 files changed, 2683 insertions(+), 3230 deletions(-) delete mode 100644 app/gui/src/dashboard/events/AssetEventType.ts delete mode 100644 app/gui/src/dashboard/events/AssetListEventType.ts delete mode 100644 app/gui/src/dashboard/events/assetEvent.ts delete mode 100644 app/gui/src/dashboard/events/assetListEvent.ts create mode 100644 app/gui/src/dashboard/hooks/backendBatchedHooks.ts create mode 100644 app/gui/src/dashboard/hooks/backendHooks.ts delete mode 100644 app/gui/src/dashboard/hooks/backendHooks.tsx create mode 100644 app/gui/src/dashboard/hooks/backendUploadFilesHooks.tsx create mode 100644 app/gui/src/dashboard/hooks/cutAndPasteHooks.ts delete mode 100644 app/gui/src/dashboard/layouts/Drive/EventListProvider.tsx create mode 100644 app/gui/src/dashboard/layouts/Drive/fetchDirectoriesHooks.ts diff --git a/.gitignore b/.gitignore index 444645571d9b..d860250369e9 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,9 @@ test-results/ test-traces/ playwright-report/ playwright/.cache/ + +######### +## Git ## +######### + +/.mailmap diff --git a/app/common/src/backendQuery.ts b/app/common/src/backendQuery.ts index 1c1ae7fafcaf..75f5054bfab2 100644 --- a/app/common/src/backendQuery.ts +++ b/app/common/src/backendQuery.ts @@ -11,7 +11,9 @@ export type BackendMethods = object.ExtractKeys) => queryCore.QueryKey + [Method in BackendMethods]?: ( + ...args: Readonly> + ) => queryCore.QueryKey } const NORMALIZE_METHOD_QUERY: BackendQueryNormalizers = { @@ -22,7 +24,7 @@ const NORMALIZE_METHOD_QUERY: BackendQueryNormalizers = { /** Creates a partial query key representing the given method and arguments. */ function normalizeMethodQuery( method: Method, - args: Parameters, + args: Readonly>, ) { return NORMALIZE_METHOD_QUERY[method]?.(...args) ?? args } @@ -31,7 +33,7 @@ function normalizeMethodQuery( export function backendQueryOptions( backend: Backend | null, method: Method, - args: Parameters, + args: Readonly>, keyExtra?: queryCore.QueryKey | undefined, ): { queryKey: queryCore.QueryKey @@ -47,7 +49,7 @@ export function backendQueryOptions( export function backendQueryKey( backend: Backend | null, method: Method, - args: Parameters, + args: Readonly>, keyExtra?: queryCore.QueryKey | undefined, ): queryCore.QueryKey { return [backend?.type, method, ...normalizeMethodQuery(method, args), ...(keyExtra ?? [])] diff --git a/app/common/src/queryClient.ts b/app/common/src/queryClient.ts index 140caf24287e..90a1d1c3c775 100644 --- a/app/common/src/queryClient.ts +++ b/app/common/src/queryClient.ts @@ -39,6 +39,7 @@ declare module '@tanstack/query-core' { * @default false */ readonly awaitInvalidates?: queryCore.QueryKey[] | boolean + readonly refetchType?: queryCore.InvalidateQueryFilters['refetchType'] } readonly queryMeta: { @@ -98,6 +99,7 @@ export function createQueryClient( mutationCache: new queryCore.MutationCache({ onSuccess: (_data, _variables, _context, mutation) => { const shouldAwaitInvalidates = mutation.meta?.awaitInvalidates ?? false + const refetchType = mutation.meta?.refetchType ?? 'active' const invalidates = mutation.meta?.invalidates ?? [] const invalidatesToAwait = (() => { if (Array.isArray(shouldAwaitInvalidates)) { @@ -113,6 +115,7 @@ export function createQueryClient( for (const queryKey of invalidatesToIgnore) { void queryClient.invalidateQueries({ predicate: query => queryCore.matchQuery({ queryKey }, query), + refetchType, }) } @@ -121,6 +124,7 @@ export function createQueryClient( invalidatesToAwait.map(queryKey => queryClient.invalidateQueries({ predicate: query => queryCore.matchQuery({ queryKey }, query), + refetchType, }), ), ) diff --git a/app/common/src/services/Backend.ts b/app/common/src/services/Backend.ts index 452d08ef633e..7a77545c63f4 100644 --- a/app/common/src/services/Backend.ts +++ b/app/common/src/services/Backend.ts @@ -1070,6 +1070,18 @@ export function assetIsType(type: Type) { return (asset: AnyAsset): asset is Extract> => asset.type === type } +/** Extract the type of an id and return a discriminated union containing both id and type. */ +export function extractTypeFromId(id: AssetId): AnyAsset extends infer T ? + T extends T ? + Pick + : never +: never { + return { + type: id.match(/^(.+?)-/)?.[1], + id, + } as never +} + /** Creates a new placeholder asset id for the given asset type. */ export function createPlaceholderAssetId( type: Type, @@ -1674,11 +1686,7 @@ export default abstract class Backend { title: string, ): Promise /** Return project details. */ - abstract getProjectDetails( - projectId: ProjectId, - directoryId: DirectoryId | null, - getPresignedUrl?: boolean, - ): Promise + abstract getProjectDetails(projectId: ProjectId, getPresignedUrl?: boolean): Promise /** Return Language Server logs for a project session. */ abstract getProjectSessionLogs( projectSessionId: ProjectSessionId, @@ -1767,8 +1775,8 @@ export default abstract class Backend { projectId?: string | null, metadata?: object | null, ): Promise - /** Download from an arbitrary URL that is assumed to originate from this backend. */ - abstract download(url: string, name?: string): Promise + /** Download an asset. */ + abstract download(assetId: AssetId, title: string): Promise /** * Get the URL for the customer portal. diff --git a/app/common/src/text/english.json b/app/common/src/text/english.json index 2a8daff68ada..41d132f95426 100644 --- a/app/common/src/text/english.json +++ b/app/common/src/text/english.json @@ -39,7 +39,6 @@ "editDescriptionError": "Could not edit description", "canOnlyDownloadFilesError": "You currently can only download files.", "noProjectSelectedError": "First select a project to download.", - "downloadInvalidTypeError": "You can only download files, projects, and Datalinks", "downloadProjectError": "Could not download project '$0'", "downloadFileError": "Could not download file '$0'", "downloadDatalinkError": "Could not download Datalink '$0'", @@ -64,9 +63,6 @@ "nameShouldNotContainInvalidCharacters": "Name should not contain invalid characters", "invalidEmailValidationError": "Please enter a valid email address", - "projectHasNoSourceFilesPhrase": "project has no source files", - "fileNotFoundPhrase": "file not found", - "noNewProfilePictureError": "Could not upload a new profile picture because no image was found", "registrationError": "Something went wrong! Please try again or contact the administrators.", diff --git a/app/common/src/utilities/data/array.ts b/app/common/src/utilities/data/array.ts index 032937f468de..37d1f335db56 100644 --- a/app/common/src/utilities/data/array.ts +++ b/app/common/src/utilities/data/array.ts @@ -1,6 +1,6 @@ /** @file Utilities for manipulating arrays. */ -export const EMPTY_ARRAY: readonly never[] = [] +export const EMPTY_ARRAY: readonly [] = [] // ==================== // === shallowEqual === diff --git a/app/common/src/utilities/data/object.ts b/app/common/src/utilities/data/object.ts index 963cafed7b4e..cd6b01233b35 100644 --- a/app/common/src/utilities/data/object.ts +++ b/app/common/src/utilities/data/object.ts @@ -219,3 +219,12 @@ export function useObjectId() { * can be used to splice together objects without the risk of collisions. */ export type DisjointKeysUnion = keyof A & keyof B extends never ? A & B : never + +/** + * Merge types of values of an object union. Useful to return an object that UNSAFELY + * (at runtime) conforms to the shape of a discriminated union. + * Especially useful for things like Tanstack Query results. + */ +export type MergeValuesOfObjectUnion = { + [K in `${keyof T & string}`]: T[K & keyof T] +} diff --git a/app/gui/integration-test/dashboard/delete.spec.ts b/app/gui/integration-test/dashboard/delete.spec.ts index 360bc8f69c9d..f1b68c344370 100644 --- a/app/gui/integration-test/dashboard/delete.spec.ts +++ b/app/gui/integration-test/dashboard/delete.spec.ts @@ -21,11 +21,6 @@ test('delete and restore', ({ page }) => .contextMenu.restoreFromTrash() .driveTable.expectTrashPlaceholderRow() .goToCategory.cloud() - .expectStartModal() - .withStartModal(async (startModal) => { - await expect(startModal).toBeVisible() - }) - .close() .driveTable.withRows(async (rows) => { await expect(rows).toHaveCount(1) })) @@ -50,8 +45,6 @@ test('delete and restore (keyboard)', ({ page }) => .press('Mod+R') .driveTable.expectTrashPlaceholderRow() .goToCategory.cloud() - .expectStartModal() - .close() .driveTable.withRows(async (rows) => { await expect(rows).toHaveCount(1) })) diff --git a/app/gui/integration-test/dashboard/labelsPanel.spec.ts b/app/gui/integration-test/dashboard/labelsPanel.spec.ts index f38f2204974a..dfcd6781f650 100644 --- a/app/gui/integration-test/dashboard/labelsPanel.spec.ts +++ b/app/gui/integration-test/dashboard/labelsPanel.spec.ts @@ -84,6 +84,7 @@ test('labels', ({ page }) => // Labels panel with one entry await locateCreateButton(locateNewLabelModal(page)).click() await expect(locateLabelsPanel(page)).toBeVisible() + expect(await locateLabelsPanelLabels(page).count()).toBe(1) // Empty labels panel again, after deleting the only entry await locateLabelsPanelLabels(page).first().hover() @@ -91,5 +92,5 @@ test('labels', ({ page }) => const labelsPanel = locateLabelsPanel(page) await labelsPanel.getByRole('button').and(labelsPanel.getByLabel(TEXT.delete)).click() await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click() - expect(await locateLabelsPanelLabels(page).count()).toBeGreaterThanOrEqual(1) + expect(await locateLabelsPanelLabels(page).count()).toBe(0) })) diff --git a/app/gui/src/dashboard/App.tsx b/app/gui/src/dashboard/App.tsx index 5580ac6e2eb2..0e786cae8728 100644 --- a/app/gui/src/dashboard/App.tsx +++ b/app/gui/src/dashboard/App.tsx @@ -560,10 +560,6 @@ function AppRouter(props: AppRouterProps) { ) } -// ==================================== -// === LocalBackendPathSynchronizer === -// ==================================== - /** Keep `localBackend.rootPath` in sync with the saved root path state. */ function LocalBackendPathSynchronizer() { const [localRootDirectory] = localStorageProvider.useLocalStorageState('localRootDirectory') @@ -575,5 +571,6 @@ function LocalBackendPathSynchronizer() { localBackend.resetRootPath() } } + return null } diff --git a/app/gui/src/dashboard/components/dashboard/AssetRow.tsx b/app/gui/src/dashboard/components/dashboard/AssetRow.tsx index 2dbbbc374bb1..de2f563cf490 100644 --- a/app/gui/src/dashboard/components/dashboard/AssetRow.tsx +++ b/app/gui/src/dashboard/components/dashboard/AssetRow.tsx @@ -1,7 +1,7 @@ /** @file A table row for an arbitrary asset. */ import * as React from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import invariant from 'tiny-invariant' import { useStore } from 'zustand' @@ -13,7 +13,10 @@ import { useEventCallback } from '#/hooks/eventCallbackHooks' import type { DrivePastePayload } from '#/providers/DriveProvider' import { useDriveStore, - useSetSelectedKeys, + useSetDragTargetAssetId, + useSetIsDraggingOverSelectedRow, + useSetLabelsDragPayload, + useSetSelectedAssets, useToggleDirectoryExpansion, } from '#/providers/DriveProvider' import * as modalProvider from '#/providers/ModalProvider' @@ -22,55 +25,40 @@ import * as textProvider from '#/providers/TextProvider' import * as assetRowUtils from '#/components/dashboard/AssetRow/assetRowUtils' import * as columnModule from '#/components/dashboard/column' import * as columnUtils from '#/components/dashboard/column/columnUtils' -import AssetEventType from '#/events/AssetEventType' -import AssetListEventType from '#/events/AssetListEventType' import AssetContextMenu from '#/layouts/AssetContextMenu' import type * as assetsTable from '#/layouts/AssetsTable' -import { isCloudCategory, isLocalCategory } from '#/layouts/CategorySwitcher/Category' -import * as eventListProvider from '#/layouts/Drive/EventListProvider' -import * as localBackend from '#/services/LocalBackend' +import { isLocalCategory } from '#/layouts/CategorySwitcher/Category' import * as backendModule from '#/services/Backend' import { Text } from '#/components/AriaComponents' import { IndefiniteSpinner } from '#/components/Spinner' -import type { AssetEvent } from '#/events/assetEvent' -import { useCutAndPaste } from '#/events/assetListEvent' import { - backendMutationOptions, - backendQueryOptions, - useBackendMutationState, - useUploadFiles, -} from '#/hooks/backendHooks' + useDeleteAssetsMutationState, + useRestoreAssetsMutationState, +} from '#/hooks/backendBatchedHooks' +import { backendMutationOptions, useBackendMutationState } from '#/hooks/backendHooks' +import { useUploadFiles } from '#/hooks/backendUploadFilesHooks' +import { useCutAndPaste } from '#/hooks/cutAndPasteHooks' import { createGetProjectDetailsQuery } from '#/hooks/projectHooks' import { useSyncRef } from '#/hooks/syncRefHooks' -import { useToastAndLog } from '#/hooks/toastAndLogHooks' import { useAsset } from '#/layouts/Drive/assetsTableItemsHooks' import { useFullUserSession } from '#/providers/AuthProvider' import type * as assetTreeNode from '#/utilities/AssetTreeNode' -import { download } from '#/utilities/download' import * as drag from '#/utilities/drag' import * as eventModule from '#/utilities/event' import * as indent from '#/utilities/indent' import * as object from '#/utilities/object' import * as permissions from '#/utilities/permissions' -import * as set from '#/utilities/set' import * as tailwindMerge from '#/utilities/tailwindMerge' import Visibility from '#/utilities/Visibility' - -// ================= -// === Constants === -// ================= +import { EMPTY_ARRAY } from 'enso-common/src/utilities/data/array' /** * The amount of time (in milliseconds) the drag item must be held over this component * to make a directory row expand. */ -const DRAG_EXPAND_DELAY_MS = 500 - -// ================ -// === AssetRow === -// ================ +const DRAG_EXPAND_DELAY_MS = 1_500 /** Common properties for state and setters passed to event handlers on an {@link AssetRow}. */ export interface AssetRowInnerProps { @@ -92,7 +80,6 @@ export interface AssetRowProps { readonly type: backendModule.AssetType readonly hidden: boolean readonly path: string - readonly initialAssetEvents: readonly AssetEvent[] | null readonly depth: number readonly state: assetsTable.AssetsTableState readonly columns: columnUtils.Column[] @@ -105,10 +92,6 @@ export interface AssetRowProps { event: React.DragEvent, item: backendModule.AnyAsset, ) => void - readonly onDragOver?: ( - event: React.DragEvent, - item: backendModule.AnyAsset, - ) => void readonly onDragLeave?: ( event: React.DragEvent, item: backendModule.AnyAsset, @@ -148,18 +131,14 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) { } }) -/** - * Props for a {@link AssetSpecialRow}. - */ +/** Props for a {@link AssetSpecialRow}. */ export interface AssetSpecialRowProps { readonly type: backendModule.AssetType readonly columnsLength: number readonly depth: number } -/** - * Renders a special asset row. - */ +/** Renders a special asset row. */ // eslint-disable-next-line no-restricted-syntax const AssetSpecialRow = React.memo(function AssetSpecialRow(props: AssetSpecialRowProps) { const { type, columnsLength, depth } = props @@ -230,14 +209,10 @@ const AssetSpecialRow = React.memo(function AssetSpecialRow(props: AssetSpecialR } }) -/** - * Props for a {@link RealAssetRow}. - */ +/** Props for a {@link RealAssetRow}. */ type RealAssetRowProps = AssetRowProps & { readonly id: backendModule.RealAssetId } -/** - * Renders a real asset row. - */ +/** Renders a real asset row. */ // eslint-disable-next-line no-restricted-syntax const RealAssetRow = React.memo(function RealAssetRow(props: RealAssetRowProps) { const { id } = props @@ -252,16 +227,12 @@ const RealAssetRow = React.memo(function RealAssetRow(props: RealAssetRowProps) return }) -/** - * Internal props for a {@link RealAssetRow}. - */ +/** Internal props for a {@link RealAssetRow}. */ export interface RealAssetRowInternalProps extends AssetRowProps { readonly asset: backendModule.AnyAsset } -/** - * Internal implementation of a {@link RealAssetRow}. - */ +/** Internal implementation of a {@link RealAssetRow}. */ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { const { id, @@ -278,14 +249,12 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { asset, } = props const { path, hidden: hiddenRaw, grabKeyboardFocus, visibility: visibilityRaw, depth } = props - const { initialAssetEvents } = props - const { nodeMap, doCopy, doCut, doPaste, doDelete: doDeleteRaw } = state - const { doRestore, doMove, category, rootDirectoryId, backend } = state + const { nodeMap, doCopy, doCut, doPaste } = state + const { category, rootDirectoryId, backend } = state const driveStore = useDriveStore() - const queryClient = useQueryClient() const { user } = useFullUserSession() - const setSelectedKeys = useSetSelectedKeys() + const setSelectedAssets = useSetSelectedAssets() const selected = useStore(driveStore, ({ visuallySelectedKeys, selectedKeys }) => (visuallySelectedKeys ?? selectedKeys).has(id), ) @@ -299,17 +268,18 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { ) const draggableProps = dragAndDropHooks.useDraggable({ isDisabled: !selected }) const { setModal, unsetModal } = modalProvider.useSetModal() - const { getText } = textProvider.useText() - const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent() const [isDraggedOver, setIsDraggedOver] = React.useState(false) + const setIsDraggingOverSelectedRow = useSetIsDraggingOverSelectedRow() + const setDragTargetAssetId = useSetDragTargetAssetId() const rootRef = React.useRef(null) const dragOverTimeoutHandle = React.useRef(null) const grabKeyboardFocusRef = useSyncRef(grabKeyboardFocus) const [innerRowState, setRowState] = React.useState( assetRowUtils.INITIAL_ROW_STATE, ) - const cutAndPaste = useCutAndPaste(category) + const cutAndPaste = useCutAndPaste(backend, category) const toggleDirectoryExpansion = useToggleDirectoryExpansion() + const setLabelsDragPayload = useSetLabelsDragPayload() const isNewlyCreated = useStore(driveStore, ({ newestFolderId }) => newestFolderId === asset.id) const isEditingName = innerRowState.isEditingName || isNewlyCreated @@ -323,16 +293,29 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { readonly parentKeys: Map } | null>(null) - const isDeleting = + const isDeletingSingleAsset = useBackendMutationState(backend, 'deleteAsset', { predicate: ({ state: { variables: [assetId] = [] } }) => assetId === asset.id, + select: () => null, }).length !== 0 - const isRestoring = + const isDeletingMultipleAssets = + useDeleteAssetsMutationState(backend, { + predicate: ({ state: { variables: [assetIds = EMPTY_ARRAY] = EMPTY_ARRAY } }) => + assetIds.includes(asset.id), + select: () => null, + }).length !== 0 + const isDeleting = isDeletingSingleAsset || isDeletingMultipleAssets + const isRestoringSingleAsset = useBackendMutationState(backend, 'undoDeleteAsset', { - predicate: ({ state: { variables: [assetId] = [] } }) => assetId === asset.id, + predicate: ({ state: { variables: [assetId] = EMPTY_ARRAY } }) => assetId === asset.id, + select: () => null, }).length !== 0 - - const isCloud = isCloudCategory(category) + const isRestoringMultipleAssets = + useRestoreAssetsMutationState(backend, { + predicate: ({ state: { variables: assetIds = EMPTY_ARRAY } }) => assetIds.includes(asset.id), + select: () => null, + }).length !== 0 + const isRestoring = isRestoringSingleAsset || isRestoringMultipleAssets const { data: projectState } = useQuery({ ...createGetProjectDetailsQuery({ @@ -340,18 +323,21 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { // see `enabled` property below. // eslint-disable-next-line no-restricted-syntax assetId: asset.id as backendModule.ProjectId, - parentId: asset.parentId, backend, }), select: (data) => data.state.type, enabled: asset.type === backendModule.AssetType.project && !isPlaceholder && isOpened, }) - const toastAndLog = useToastAndLog() - const uploadFiles = useUploadFiles(backend, category) - const createPermissionMutation = useMutation(backendMutationOptions(backend, 'createPermission')) - const associateTagMutation = useMutation(backendMutationOptions(backend, 'associateTag')) + const createPermissionMutation = useMutation( + backendMutationOptions(backend, 'createPermission', { + meta: { + invalidates: [[backend.type, 'listDirectory', asset.parentId]], + awaitInvalidates: true, + }, + }), + ) const insertionVisibility = useStore(driveStore, (driveState) => driveState.pasteData?.type === 'move' && driveState.pasteData.data.ids.has(id) ? @@ -360,19 +346,22 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { ) const createPermissionVariables = createPermissionMutation.variables?.[0] const isRemovingSelf = - createPermissionVariables != null && - createPermissionVariables.action == null && - createPermissionVariables.actorsIds[0] === user.userId + createPermissionVariables?.actorsIds[0] === user.userId && + createPermissionVariables.action == null const visibility = - isRemovingSelf ? Visibility.hidden + isDeleting || isRestoring ? Visibility.faded + : isRemovingSelf ? Visibility.hidden : visibilityRaw === Visibility.visible ? insertionVisibility : visibilityRaw ?? insertionVisibility - const hidden = isDeleting || isRestoring || hiddenRaw || visibility === Visibility.hidden + const hidden = hiddenRaw || visibility === Visibility.hidden const setSelected = useEventCallback((newSelected: boolean) => { - const { selectedKeys } = driveStore.getState() - - setSelectedKeys(set.withPresence(selectedKeys, id, newSelected)) + const { selectedAssets } = driveStore.getState() + setSelectedAssets( + newSelected ? + [...selectedAssets, asset] + : selectedAssets.filter((otherAsset) => otherAsset.id !== asset.id), + ) }) React.useEffect(() => { @@ -388,27 +377,22 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { } }, [grabKeyboardFocusRef, isKeyboardSelected, asset]) - const doDelete = React.useCallback( - (forever = false) => { - void doDeleteRaw(asset, forever) - }, - [doDeleteRaw, asset], - ) - - const clearDragState = React.useCallback(() => { - setIsDraggedOver(false) - setRowState((oldRowState) => - oldRowState.temporarilyAddedLabels === set.EMPTY_SET ? - oldRowState - : object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }), - ) - }, []) - const onDragOver = (event: React.DragEvent) => { - const directoryKey = asset.type === backendModule.AssetType.directory ? id : parentId + const directoryId = asset.type === backendModule.AssetType.directory ? id : parentId + const labelsPayload = drag.LABELS.lookup(event) + if (labelsPayload) { + event.preventDefault() + event.stopPropagation() + setDragTargetAssetId(asset.id) + const { isDraggingOverSelectedRow } = driveStore.getState() + if (selected !== isDraggingOverSelectedRow) { + setIsDraggingOverSelectedRow(selected) + } + return + } const payload = drag.ASSET_ROWS.lookup(event) const isPayloadMatch = - payload != null && payload.every((innerItem) => innerItem.key !== directoryKey) + payload != null && payload.every((innerItem) => innerItem.key !== directoryId) const canPaste = (() => { if (!isPayloadMatch) { return false @@ -417,7 +401,7 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { const parentKeys = new Map( Array.from(nodeMap.current.entries()).map(([otherId, otherAsset]) => [ otherId, - otherAsset.directoryKey, + otherAsset.item.parentId, ]), ) nodeParentKeysRef.current = { nodeMap: new WeakRef(nodeMap.current), parentKeys } @@ -454,212 +438,6 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { } } - eventListProvider.useAssetEventListener(async (event) => { - switch (event.type) { - case AssetEventType.move: { - if (event.ids.has(id)) { - await doMove(event.newParentKey, asset) - } - break - } - case AssetEventType.delete: { - if (event.ids.has(id)) { - doDelete(false) - } - break - } - case AssetEventType.deleteForever: { - if (event.ids.has(id)) { - doDelete(true) - } - break - } - case AssetEventType.restore: { - if (event.ids.has(id)) { - await doRestore(asset) - } - break - } - case AssetEventType.download: - case AssetEventType.downloadSelected: { - if (event.type === AssetEventType.downloadSelected ? selected : event.ids.has(asset.id)) { - if (isCloud) { - switch (asset.type) { - case backendModule.AssetType.project: { - try { - const details = await queryClient.fetchQuery( - backendQueryOptions( - backend, - 'getProjectDetails', - [asset.id, asset.parentId, true], - { staleTime: 0 }, - ), - ) - if (details.url != null) { - await backend.download(details.url, `${asset.title}.enso-project`) - } else { - const error: unknown = getText('projectHasNoSourceFilesPhrase') - toastAndLog('downloadProjectError', error, asset.title) - } - } catch (error) { - toastAndLog('downloadProjectError', error, asset.title) - } - break - } - case backendModule.AssetType.file: { - try { - const details = await queryClient.fetchQuery( - backendQueryOptions(backend, 'getFileDetails', [asset.id, asset.title, true], { - staleTime: 0, - }), - ) - if (details.url != null) { - await backend.download(details.url, asset.title) - } else { - const error: unknown = getText('fileNotFoundPhrase') - toastAndLog('downloadFileError', error, asset.title) - } - } catch (error) { - toastAndLog('downloadFileError', error, asset.title) - } - break - } - case backendModule.AssetType.datalink: { - try { - const value = await queryClient.fetchQuery( - backendQueryOptions(backend, 'getDatalink', [asset.id, asset.title]), - ) - const fileName = `${asset.title}.datalink` - download( - URL.createObjectURL( - new File([JSON.stringify(value)], fileName, { - type: 'application/json+x-enso-data-link', - }), - ), - fileName, - ) - } catch (error) { - toastAndLog('downloadDatalinkError', error, asset.title) - } - break - } - default: { - toastAndLog('downloadInvalidTypeError') - break - } - } - } else { - if (asset.type === backendModule.AssetType.project) { - const projectsDirectory = localBackend.extractTypeAndId(asset.parentId).id - const uuid = localBackend.extractTypeAndId(asset.id).id - const queryString = new URLSearchParams({ projectsDirectory }).toString() - await backend.download( - `./api/project-manager/projects/${uuid}/enso-project?${queryString}`, - `${asset.title}.enso-project`, - ) - } - } - } - break - } - case AssetEventType.removeSelf: { - // This is not triggered from the asset list, so it uses `item.id` instead of `key`. - if (event.id === asset.id && user.isEnabled) { - try { - await createPermissionMutation.mutateAsync([ - { - action: null, - resourceId: asset.id, - actorsIds: [user.userId], - }, - ]) - dispatchAssetListEvent({ type: AssetListEventType.delete, key: id }) - } catch (error) { - toastAndLog(null, error) - } - } - break - } - case AssetEventType.temporarilyAddLabels: { - const labels = event.ids.has(id) ? event.labelNames : set.EMPTY_SET - setRowState((oldRowState) => - ( - oldRowState.temporarilyAddedLabels === labels && - oldRowState.temporarilyRemovedLabels === set.EMPTY_SET - ) ? - oldRowState - : object.merge(oldRowState, { - temporarilyAddedLabels: labels, - temporarilyRemovedLabels: set.EMPTY_SET, - }), - ) - break - } - case AssetEventType.temporarilyRemoveLabels: { - const labels = event.ids.has(id) ? event.labelNames : set.EMPTY_SET - setRowState((oldRowState) => - ( - oldRowState.temporarilyAddedLabels === set.EMPTY_SET && - oldRowState.temporarilyRemovedLabels === labels - ) ? - oldRowState - : object.merge(oldRowState, { - temporarilyAddedLabels: set.EMPTY_SET, - temporarilyRemovedLabels: labels, - }), - ) - break - } - case AssetEventType.addLabels: { - setRowState((oldRowState) => - oldRowState.temporarilyAddedLabels === set.EMPTY_SET ? - oldRowState - : object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }), - ) - const labels = asset.labels - if ( - event.ids.has(id) && - (labels == null || [...event.labelNames].some((label) => !labels.includes(label))) - ) { - const newLabels = [ - ...(labels ?? []), - ...[...event.labelNames].filter((label) => labels?.includes(label) !== true), - ] - try { - await associateTagMutation.mutateAsync([asset.id, newLabels, asset.title]) - } catch (error) { - toastAndLog(null, error) - } - } - break - } - case AssetEventType.removeLabels: { - setRowState((oldRowState) => - oldRowState.temporarilyAddedLabels === set.EMPTY_SET ? - oldRowState - : object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }), - ) - const labels = asset.labels - if ( - event.ids.has(id) && - labels != null && - [...event.labelNames].some((label) => labels.includes(label)) - ) { - const newLabels = labels.filter((label) => !event.labelNames.has(label)) - try { - await associateTagMutation.mutateAsync([asset.id, newLabels, asset.title]) - } catch (error) { - toastAndLog(null, error) - } - } - break - } - default: { - return - } - } - }, initialAssetEvents) - switch (type) { case backendModule.AssetType.directory: case backendModule.AssetType.project: @@ -734,7 +512,6 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { doCopy={doCopy} doCut={doCut} doPaste={doPaste} - doDelete={doDelete} />, ) } @@ -761,18 +538,17 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { }, DRAG_EXPAND_DELAY_MS) } // Required because `dragover` does not fire on `mouseenter`. - props.onDragOver?.(event, asset) onDragOver(event) }} onDragOver={(event) => { if (state.category.type === 'trash') { event.dataTransfer.dropEffect = 'none' } - props.onDragOver?.(event, asset) onDragOver(event) }} onDragEnd={(event) => { - clearDragState() + setIsDraggedOver(false) + setLabelsDragPayload(null) props.onDragEnd?.(event, asset) }} onDragLeave={(event) => { @@ -787,14 +563,14 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { event.relatedTarget instanceof Node && !event.currentTarget.contains(event.relatedTarget) ) { - clearDragState() + setIsDraggedOver(false) } props.onDragLeave?.(event, asset) }} onDrop={(event) => { if (state.category.type !== 'trash') { props.onDrop?.(event, asset) - clearDragState() + setIsDraggedOver(false) const directoryId = asset.type === backendModule.AssetType.directory ? asset.id : parentId const payload = drag.ASSET_ROWS.lookup(event) @@ -831,7 +607,6 @@ export function RealAssetInternalRow(props: RealAssetRowInternalProps) { )} diff --git a/app/gui/src/dashboard/components/dashboard/Label.tsx b/app/gui/src/dashboard/components/dashboard/Label.tsx index 7420a08b7ec0..4597d982cb19 100644 --- a/app/gui/src/dashboard/components/dashboard/Label.tsx +++ b/app/gui/src/dashboard/components/dashboard/Label.tsx @@ -65,7 +65,7 @@ export default function Label(props: InternalLabelProps) { title={title} disabled={isDisabled} className={twMerge( - 'focus-child relative flex h-6 items-center whitespace-nowrap rounded-inherit px-[7px] opacity-75 transition-all after:pointer-events-none after:absolute after:inset after:rounded-full hover:opacity-100 focus:opacity-100', + 'focus-child relative flex h-6 items-center whitespace-nowrap rounded-inherit px-[7px] opacity-50 transition-all after:pointer-events-none after:absolute after:inset after:rounded-full hover:opacity-100 focus:opacity-100', active && 'active', negated && 'after:border-2 after:border-delete', )} diff --git a/app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx b/app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx index 8d580e96c13c..fbb05d81f6b9 100644 --- a/app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx +++ b/app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx @@ -93,7 +93,6 @@ export default function ProjectIcon(props: ProjectIconProps) { const { data: projectState, isError } = reactQuery.useQuery({ ...projectHooks.createGetProjectDetailsQuery({ assetId: item.id, - parentId: item.parentId, backend, }), select: (data) => data.state, diff --git a/app/gui/src/dashboard/components/dashboard/column.ts b/app/gui/src/dashboard/components/dashboard/column.ts index 9ea88144c16f..d021d0045f12 100644 --- a/app/gui/src/dashboard/components/dashboard/column.ts +++ b/app/gui/src/dashboard/components/dashboard/column.ts @@ -3,7 +3,7 @@ import { memo, type Dispatch, type JSX, type SetStateAction } from 'react' import type { AssetRowState, AssetsTableState } from '#/layouts/AssetsTable' import type { Category } from '#/layouts/CategorySwitcher/Category' -import type { AnyAsset, Asset, AssetId, BackendType } from '#/services/Backend' +import type { AnyAsset, Asset, BackendType } from '#/services/Backend' import type { SortInfo } from '#/utilities/sorting' import type { SortableColumn } from './column/columnUtils' import { Column } from './column/columnUtils' @@ -15,13 +15,8 @@ import PathColumn from './column/PathColumn' import PlaceholderColumn from './column/PlaceholderColumn' import SharedWithColumn from './column/SharedWithColumn' -// =================== -// === AssetColumn === -// =================== - /** Props for an arbitrary variant of {@link Asset}. */ export interface AssetColumnProps { - readonly keyProp: AssetId readonly isOpened: boolean readonly item: AnyAsset readonly depth: number diff --git a/app/gui/src/dashboard/components/dashboard/column/LabelsColumn.tsx b/app/gui/src/dashboard/components/dashboard/column/LabelsColumn.tsx index bdb1959cd322..1884b436a3b2 100644 --- a/app/gui/src/dashboard/components/dashboard/column/LabelsColumn.tsx +++ b/app/gui/src/dashboard/components/dashboard/column/LabelsColumn.tsx @@ -19,7 +19,10 @@ import ManageLabelsModal from '#/modals/ManageLabelsModal' import * as backendModule from '#/services/Backend' +import { useStore } from '#/hooks/storeHooks' +import { useDriveStore } from '#/providers/DriveProvider' import * as permissions from '#/utilities/permissions' +import { EMPTY_ARRAY } from 'enso-common/src/utilities/data/array' // ==================== // === LabelsColumn === @@ -27,13 +30,18 @@ import * as permissions from '#/utilities/permissions' /** A column listing the labels on this asset. */ export default function LabelsColumn(props: column.AssetColumnProps) { - const { item, state, rowState } = props + const { item, state } = props const { backend, category, setQuery } = state - const { temporarilyAddedLabels, temporarilyRemovedLabels } = rowState const { user } = authProvider.useFullUserSession() const { setModal, unsetModal } = modalProvider.useSetModal() const { getText } = textProvider.useText() const { data: labels } = backendHooks.useBackendQuery(backend, 'listTags', []) + const driveStore = useDriveStore() + const showDraggedLabelsFallback = useStore( + driveStore, + ({ selectedKeys, isDraggingOverSelectedRow }) => + isDraggingOverSelectedRow && selectedKeys.has(item.id), + ) const labelsByName = React.useMemo(() => { return new Map(labels?.map((label) => [label.value, label])) }, [labels]) @@ -42,6 +50,38 @@ export default function LabelsColumn(props: column.AssetColumnProps) { category.type !== 'trash' && (self?.permission === permissions.PermissionAction.own || self?.permission === permissions.PermissionAction.admin) + const temporarilyAddedLabels = useStore( + driveStore, + ({ labelsDragPayload, dragTargetAssetId }) => { + const areTemporaryLabelsRelevant = (() => { + if (showDraggedLabelsFallback) { + return labelsDragPayload?.typeWhenAppliedToSelection === 'add' + } else { + return item.id === dragTargetAssetId + } + })() + if (areTemporaryLabelsRelevant) { + return labelsDragPayload?.labels ?? EMPTY_ARRAY + } + return EMPTY_ARRAY + }, + ) + const temporarilyRemovedLabels = useStore( + driveStore, + ({ labelsDragPayload, dragTargetAssetId }) => { + const areTemporaryLabelsRelevant = (() => { + if (showDraggedLabelsFallback) { + return labelsDragPayload?.typeWhenAppliedToSelection === 'remove' + } else { + return item.id === dragTargetAssetId + } + })() + if (areTemporaryLabelsRelevant) { + return labelsDragPayload?.labels ?? EMPTY_ARRAY + } + return EMPTY_ARRAY + }, + ) return (

@@ -53,9 +93,9 @@ export default function LabelsColumn(props: column.AssetColumnProps) { data-testid="asset-label" title={getText('rightClickToRemoveLabel')} color={labelsByName.get(label)?.color ?? backendModule.COLORS[0]} - active={!temporarilyRemovedLabels.has(label)} - isDisabled={temporarilyRemovedLabels.has(label)} - negated={temporarilyRemovedLabels.has(label)} + active={!temporarilyRemovedLabels.includes(label)} + isDisabled={temporarilyRemovedLabels.includes(label)} + negated={temporarilyRemovedLabels.includes(label)} onContextMenu={(event) => { event.preventDefault() event.stopPropagation() @@ -83,7 +123,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) { {label} ))} - {...[...temporarilyAddedLabels] + {temporarilyAddedLabels .filter((label) => item.labels?.includes(label) !== true) .map((label) => (
diff --git a/app/gui/src/dashboard/layouts/Editor.tsx b/app/gui/src/dashboard/layouts/Editor.tsx index f8eb85580168..72746fae0547 100644 --- a/app/gui/src/dashboard/layouts/Editor.tsx +++ b/app/gui/src/dashboard/layouts/Editor.tsx @@ -30,10 +30,6 @@ const ProjectViewTab = applyPureVueInReact(ProjectViewTabVue) as ( props: ProjectViewTabProps, ) => JSX.Element -// ============== -// === Editor === -// ============== - /** Props for an {@link Editor}. */ export interface EditorProps { readonly isOpeningFailed: boolean @@ -54,7 +50,6 @@ function Editor(props: EditorProps) { const projectStatusQuery = projectHooks.createGetProjectDetailsQuery({ assetId: project.id, - parentId: project.parentId, backend, }) diff --git a/app/gui/src/dashboard/layouts/GlobalContextMenu.tsx b/app/gui/src/dashboard/layouts/GlobalContextMenu.tsx index 67c3eb8ae9ac..a3878da25ff9 100644 --- a/app/gui/src/dashboard/layouts/GlobalContextMenu.tsx +++ b/app/gui/src/dashboard/layouts/GlobalContextMenu.tsx @@ -7,13 +7,8 @@ import ContextMenuEntry from '#/components/ContextMenuEntry' import UpsertDatalinkModal from '#/modals/UpsertDatalinkModal' import UpsertSecretModal from '#/modals/UpsertSecretModal' -import { - useNewDatalink, - useNewFolder, - useNewProject, - useNewSecret, - useUploadFiles, -} from '#/hooks/backendHooks' +import { useNewDatalink, useNewFolder, useNewProject, useNewSecret } from '#/hooks/backendHooks' +import { useUploadFiles } from '#/hooks/backendUploadFilesHooks' import { useEventCallback } from '#/hooks/eventCallbackHooks' import type { Category } from '#/layouts/CategorySwitcher/Category' import { useDriveStore } from '#/providers/DriveProvider' @@ -31,7 +26,6 @@ export interface GlobalContextMenuProps { readonly backend: Backend readonly category: Category readonly rootDirectoryId: DirectoryId - readonly directoryKey: DirectoryId | null readonly directoryId: DirectoryId | null readonly path: string | null readonly doPaste: (newParentKey: DirectoryId, newParentId: DirectoryId) => void @@ -49,7 +43,6 @@ export const GlobalContextMenu = function GlobalContextMenu(props: GlobalContext hidden = false, backend, category, - directoryKey = null, directoryId = null, path, rootDirectoryId, @@ -148,7 +141,7 @@ export const GlobalContextMenu = function GlobalContextMenu(props: GlobalContext }} /> )} - {isCloud && directoryKey == null && hasPasteData && ( + {isCloud && directoryId == null && hasPasteData && (