Skip to content

Commit

Permalink
WIP compiler API
Browse files Browse the repository at this point in the history
  • Loading branch information
jmbockhorst committed Aug 31, 2021
1 parent 83c6fbc commit 50b2132
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 148 deletions.
152 changes: 97 additions & 55 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fs from "fs";
import globby from "globby";
import os from "os";
import "reflect-metadata";
import { container } from "tsyringe";
import util from "util";
import { Connection } from "vscode-languageserver";
import { URI } from "vscode-uri";
import Parser, { Tree } from "web-tree-sitter";
import { ICancellationToken } from "../cancellation";
Expand All @@ -17,7 +17,7 @@ import {
IPossibleImportsCache,
PossibleImportsCache,
} from "../util/possibleImportsCache";
import { Settings } from "../util/settings";
import { getDefaultSettings, IClientSettings } from "../util/settings";
import { Diagnostic } from "./diagnostics";
import { TypeCache } from "./typeCache";
import {
Expand All @@ -26,7 +26,8 @@ import {
TypeChecker,
} from "./typeChecker";
import chokidar from "chokidar";
import { CommandManager } from "../commandManager";
import { Connection } from "vscode-languageserver";
import { loadParser } from "../parser";

const readFile = util.promisify(fs.readFile);

Expand Down Expand Up @@ -87,6 +88,7 @@ export interface IProgram {
sourceFile: ISourceFile,
importableModuleName: string,
): ISourceFile | undefined;
getSourceFiles(): ISourceFile[];
getForest(synchronize?: boolean): IForest;
getRootPath(): URI;
getTypeCache(): TypeCache;
Expand Down Expand Up @@ -135,16 +137,34 @@ interface IElmPackage extends IElmProject {
exposedModules: Set<string>;
}

export async function createProgram(
rootPath: URI,
programHost?: IProgramHost,
settings?: IClientSettings,
progressCallback?: (percent: number) => void,
): Promise<IProgram> {
const program = new Program(rootPath, programHost, settings);
await program.init(progressCallback);
return program;
}

export interface IProgramHost {
readFile(uri: string): Promise<string>;
readDirectory(uri: string): Promise<string[]>;
watchFile(uri: string, callback: () => void): void;
logger: {
info(message: string): void;
warn(message: string): void;
error(message: string): void;
};
handleError(message: string): void;
onServerDidRestart(
handler: (progressReporter: (progress: number) => void) => Promise<void>,
): void;
}

export class Program implements IProgram {
private parser: Parser;
private connection: Connection;
private settings: Settings;
private parser!: Parser;
private typeCache: TypeCache;
private typeChecker: TypeChecker | undefined;
private dirty = true;
Expand All @@ -157,24 +177,27 @@ export class Program implements IProgram {
private resolvedPackageCache = new Map<string, IElmPackage>();
private host: IProgramHost;
private filesWatching = new Set<string>();
private settings: IClientSettings;

constructor(private rootPath: URI, programHost?: IProgramHost) {
this.settings = container.resolve("Settings");
this.connection = container.resolve("Connection");
this.parser = container.resolve("Parser");
this.connection.console.info(
`Starting language server for folder: ${this.rootPath.toString()}`,
);

constructor(
private rootPath: URI,
programHost?: IProgramHost,
settings?: IClientSettings,
) {
this.typeCache = new TypeCache();
this.possibleImportsCache = new PossibleImportsCache();
this.operatorsCache = new Map<string, DefinitionResult>();
this.diagnosticsCache = new Map<string, Diagnostic[]>();
this.host = programHost ?? createNodeProgramHost();
this.settings = settings ?? getDefaultSettings();

this.host.logger.info(
`Starting language server for folder: ${this.rootPath.toString()}`,
);
}

public async init(
progressCallback: (percent: number) => void,
progressCallback?: (percent: number) => void,
): Promise<void> {
await this.initWorkspace(progressCallback);
}
Expand Down Expand Up @@ -215,6 +238,10 @@ export class Program implements IProgram {
}
}

public getSourceFiles(): ISourceFile[] {
return Array.from(this.getForest().treeMap.values());
}

public getForest(synchronize = true): IForest {
if (this.dirty && synchronize) {
this.forest.synchronize();
Expand All @@ -238,7 +265,10 @@ export class Program implements IProgram {
this.dirty = false;
}

return this.typeChecker ?? (this.typeChecker = createTypeChecker(this));
return (
this.typeChecker ??
(this.typeChecker = createTypeChecker(this, this.host))
);
}

public markAsDirty(): void {
Expand Down Expand Up @@ -323,41 +353,34 @@ export class Program implements IProgram {
}

private async initWorkspace(
progressCallback: (percent: number) => void,
progressCallback?: (percent: number) => void,
): Promise<void> {
const clientSettings = await this.settings.getClientSettings();
if (!container.isRegistered("Parser")) {
await loadParser(this.host);
}

this.parser = container.resolve("Parser");

let progress = 0;
let elmVersion;
try {
elmVersion = utils.getElmVersion(
clientSettings,
this.rootPath,
this.connection,
);
elmVersion = utils.getElmVersion(this.settings, this.rootPath, this.host);
} catch (error) {
this.connection.console.warn(
this.host.logger.warn(
`Could not figure out elm version, this will impact how good the server works. \n ${error.stack}`,
);
}

const pathToElmJson = path.join(this.rootPath.fsPath, "elm.json");
this.connection.console.info(`Reading elm.json from ${pathToElmJson}`);
this.host.logger.info(`Reading elm.json from ${pathToElmJson}`);

if (!this.filesWatching.has(pathToElmJson)) {
this.host.watchFile(pathToElmJson, () => {
void this.connection.window
.createWorkDoneProgress()
.then((progress) => {
progress.begin("Restarting Elm Language Server", 0);

this.initWorkspace((percent: number) => {
progress.report(percent, `${percent.toFixed(0)}%`);
})
.then(() => progress.done())
.catch(() => {
//
});
this.host.onServerDidRestart(async (progressReporter) => {
await this.initWorkspace((percent: number) => {
progressReporter(percent);
});
});
});
this.filesWatching.add(pathToElmJson);
}
Expand All @@ -371,11 +394,11 @@ export class Program implements IProgram {
// Run `elm make` to download dependencies
try {
utils.execCmdSync(
clientSettings.elmPath,
this.settings.elmPath,
"elm",
{ cmdArguments: ["make"] },
this.rootPath.fsPath,
this.connection,
this.host,
);
} catch (error) {
// On application projects, this will give a NO INPUT error message, but will still download the dependencies
Expand All @@ -386,38 +409,38 @@ export class Program implements IProgram {
this.forest = new Forest(this.rootProject);

const elmFilePaths = await this.findElmFilesInProject(this.rootProject);
this.connection.console.info(
this.host.logger.info(
`Found ${elmFilePaths.length.toString()} files to add to the project`,
);

if (elmFilePaths.every((a) => a.project !== this.rootProject)) {
this.connection.window.showErrorMessage(
this.host.handleError(
"The path or paths you entered in the 'source-directories' field of your 'elm.json' does not contain any elm files.",
);
}

const promiseList: Promise<void>[] = [];
const PARSE_STAGES = 3;
const PARSE_STAGES = 2;
const progressDelta = 100 / (elmFilePaths.length * PARSE_STAGES);
for (const filePath of elmFilePaths) {
progressCallback((progress += progressDelta));
if (progressCallback) {
progressCallback((progress += progressDelta));
}
promiseList.push(
this.readAndAddToForest(filePath, () => {
progressCallback((progress += progressDelta));
if (progressCallback) {
progressCallback((progress += progressDelta));
}
}),
);
}
await Promise.all(promiseList);

this.findExposedModulesOfDependencies(this.rootProject);

CommandManager.initHandlers(this.connection);

this.connection.console.info(
`Done parsing all files for ${pathToElmJson}`,
);
this.host.logger.info(`Done parsing all files for ${pathToElmJson}`);
} catch (error) {
this.connection.console.error(
this.host.logger.error(
`Error parsing files for ${pathToElmJson}:\n${error.stack}`,
);
}
Expand Down Expand Up @@ -467,7 +490,7 @@ export class Program implements IProgram {
);

if (!solvedVersions) {
this.connection.window.showErrorMessage(
this.host.handleError(
"There is a problem with elm.json. Could not solve dependencies with the given constraints. Try running `elm make` to install missing dependencies.",
);
throw new Error("Unsolvable package constraints");
Expand Down Expand Up @@ -614,7 +637,7 @@ export class Program implements IProgram {
const maintainerAndPackageName =
project.type === "package" ? project.maintainerAndPackageName : undefined;

this.connection.console.info(`Glob ${sourceDir}/**/*.elm`);
this.host.logger.info(`Glob ${sourceDir}/**/*.elm`);

(await this.host.readDirectory(sourceDir)).forEach((matchingPath) => {
matchingPath = normalizeUri(matchingPath);
Expand Down Expand Up @@ -668,7 +691,7 @@ export class Program implements IProgram {
callback: () => void,
): Promise<void> {
try {
this.connection.console.info(`Adding ${filePath.path.toString()}`);
this.host.logger.info(`Adding ${filePath.path.toString()}`);
const fileContent: string = await this.host.readFile(
filePath.path.toString(),
);
Expand All @@ -685,7 +708,7 @@ export class Program implements IProgram {
);
callback();
} catch (error) {
this.connection.console.error(error.stack);
this.host.logger.error(error.stack);
}
}

Expand Down Expand Up @@ -716,7 +739,7 @@ export class Program implements IProgram {
}
}

export function createNodeProgramHost(): IProgramHost {
export function createNodeProgramHost(connection?: Connection): IProgramHost {
return {
readFile: (uri): Promise<string> =>
readFile(uri, {
Expand All @@ -730,5 +753,24 @@ export function createNodeProgramHost(): IProgramHost {
watchFile: (uri: string, callback: () => void): void => {
chokidar.watch(uri).on("change", callback);
},
logger: connection?.console ?? {
info: (): void => {
//
},
warn: (): void => {
//
},
error: (): void => {
//
},
},
handleError:
connection?.window.showErrorMessage.bind(createNodeProgramHost) ??
((): void => {
//
}),
onServerDidRestart: (): void => {
//
},
};
}
12 changes: 6 additions & 6 deletions src/compiler/typeChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
EUnionVariant,
EPortAnnotation,
} from "./utils/expressionTree";
import { IProgram } from "./program";
import { container } from "tsyringe";
import { Connection } from "vscode-languageserver";
import { IProgram, IProgramHost } from "./program";
import {
Type,
TUnknown,
Expand Down Expand Up @@ -97,7 +95,10 @@ export interface TypeChecker {
) => SyntaxNode[];
}

export function createTypeChecker(program: IProgram): TypeChecker {
export function createTypeChecker(
program: IProgram,
host: IProgramHost,
): TypeChecker {
const forest = program.getForest();
const imports = new Map<string, Imports>();

Expand Down Expand Up @@ -242,8 +243,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {

return TUnknown;
} catch (error) {
const connection = container.resolve<Connection>("Connection");
connection.console.warn(`Error while trying to infer a type. ${error}`);
host.logger.warn(`Error while trying to infer a type. ${error}`);
return TUnknown;
}
}
Expand Down
Loading

0 comments on commit 50b2132

Please sign in to comment.