edacation
Version:
Library and CLI for interacting with Yosys and nextpnr.
372 lines • 14.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Project = exports.ProjectOutputFile = exports.ProjectInputFile = void 0;
const util_js_1 = require("../util.js");
const configuration_js_1 = require("./configuration.js");
class ProjectInputFile {
constructor(_path, _type) {
this._path = _path;
this._type = _type;
}
get path() {
return this._path;
}
get type() {
return this._type;
}
set type(type) {
this._type = type;
}
serialize() {
return {
path: this.path,
type: this.type
};
}
static deserialize(data, ..._args) {
// Older versions of this module (<= 0.3.9) stored input files as an array of paths instead,
// so we need to migrate if data is a string (single output file).
if (typeof data === 'string') {
data = { path: data, type: 'design' };
}
return new ProjectInputFile(data.path, data.type);
}
copy() {
return ProjectInputFile.deserialize(this.serialize());
}
}
exports.ProjectInputFile = ProjectInputFile;
class ProjectOutputFile {
constructor(_project, _path, _targetId = null, _stale = false) {
this._project = _project;
this._path = _path;
this._targetId = _targetId;
this._stale = _stale;
}
get path() {
return this._path;
}
get targetId() {
return this._targetId;
}
set targetId(id) {
if (id !== null && this._project.getTarget(id) === null) {
throw new Error(`Invalid target id: ${id}`);
}
this._targetId = id;
}
get target() {
if (!this._targetId)
return null;
return this._project.getTarget(this._targetId);
}
get stale() {
return this._stale;
}
set stale(isStale) {
this._stale = isStale;
}
serialize() {
return {
path: this.path,
targetId: this.targetId,
stale: this.stale
};
}
static deserialize(project, data, ..._args) {
// Older versions of this module (<= 0.3.12) stored output files as an array of paths instead,
// so we need to migrate if data is a string (single output file).
if (typeof data === 'string') {
data = { path: data, targetId: null, stale: false };
}
return new ProjectOutputFile(project, data.path, data.targetId, data.stale);
}
copy(project) {
return ProjectOutputFile.deserialize(project, this.serialize());
}
}
exports.ProjectOutputFile = ProjectOutputFile;
class Project {
constructor(name, inputFiles = [], outputFiles = [], configuration = configuration_js_1.DEFAULT_CONFIGURATION, eventCallback) {
this.batchedEvents = new Set();
this.batchCounter = 0;
this.name = name;
this.inputFiles = inputFiles.map((file) => ProjectInputFile.deserialize(file));
this.outputFiles = outputFiles.map((file) => ProjectOutputFile.deserialize(this, file));
const config = configuration_js_1.schemaProjectConfiguration.safeParse(configuration);
if (config.success) {
this.configuration = config.data;
}
else {
throw new Error(`Failed to parse project configuration: ${config.error.toString()}`);
}
// Trigger a config 'update' to deploy any modifications it might want to make
this.updateConfiguration({});
// Set event callback LAST to prevent firing events in constructor
this.eventCallback = eventCallback;
}
getName() {
return this.name;
}
getInputFiles() {
return this.inputFiles;
}
hasInputFile(filePath) {
return this.getInputFile(filePath) !== null;
}
getInputFile(filePath) {
return this.inputFiles.find((file) => file.path === filePath) ?? null;
}
addInputFiles(files) {
for (const file of files) {
if (!this.hasInputFile(file.path)) {
const inputFile = new ProjectInputFile(file.path, file.type ?? 'design');
this.inputFiles.push(inputFile);
}
}
this.inputFiles.sort((a, b) => {
return a < b ? -1 : 1;
});
this.expireOutputFiles();
}
removeInputFiles(filePaths) {
this.inputFiles = this.inputFiles.filter((file) => !filePaths.includes(file.path));
this.expireOutputFiles();
}
getOutputFiles() {
return this.outputFiles;
}
hasOutputFile(filePath) {
return this.getOutputFile(filePath) !== null;
}
getOutputFile(filePath) {
return this.outputFiles.find((file) => file.path === filePath) ?? null;
}
addOutputFiles(files) {
for (const file of files) {
const existingOutFile = this.getOutputFile(file.path);
if (existingOutFile) {
// File already exists, so we don't want to add it again.
// But, we should make sure the target ID gets updated and set `stale` to false.
existingOutFile.targetId = file.targetId;
existingOutFile.stale = false;
continue;
}
const outputFile = new ProjectOutputFile(this, file.path, file.targetId);
if (outputFile.target === null)
throw new Error(`Invalid target ID: ${file.targetId}`);
this.outputFiles.push(outputFile);
}
this.outputFiles.sort((a, b) => {
return a < b ? -1 : 1;
});
}
removeOutputFiles(filePaths) {
this.outputFiles = this.outputFiles.filter((file) => !filePaths.includes(file.path));
}
expireOutputFiles() {
if (!this.outputFiles.length)
return;
let didUpdate = false;
for (const file of this.outputFiles) {
if (!file.stale) {
file.stale = true;
didUpdate = true;
}
}
if (didUpdate)
this.emitEvents('outputFiles');
}
setTopLevelModule(targetId, module) {
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
// Ensure the config tree exists
// We don't care about setting missing defaults, as this is target-level configuration,
// so any missing properties will fallback to project-level config.
if (!target.yosys)
target.yosys = {};
if (!target.yosys.options)
target.yosys.options = {};
target.yosys.options.topLevelModule = module;
}
setTestbenchPath(targetId, testbenchPath) {
const testbenchFiles = this.getInputFiles()
.filter((file) => file.type === 'testbench')
.map((file) => file.path);
if (testbenchPath && !testbenchFiles.includes(testbenchPath))
throw new Error(`Testbench ${testbenchPath} is not marked as such!`);
const target = this.getTarget(targetId);
if (!target)
throw new Error(`Target "${targetId}" does not exist!`);
// Ensure the config tree exists
// We don't care about setting missing defaults, as this is target-level configuration,
// so any missing properties will fallback to project-level config.
if (!target.iverilog)
target.iverilog = {};
if (!target.iverilog.options)
target.iverilog.options = {};
target.iverilog.options.testbenchFile = testbenchPath;
}
setInputFileType(filePath, type) {
const file = this.getInputFile(filePath);
if (!file) {
console.warn(`Tried to set file type of missing input file: ${filePath}`);
return;
}
file.type = type;
}
getTarget(id) {
const targets = this.configuration.targets;
return targets.find((target) => target.id === id) ?? null;
}
getConfiguration() {
return this.configuration;
}
updateConfiguration(configuration) {
this.configuration = {
...this.configuration,
...configuration
};
// Unset 'lingering' output file target IDs
for (const outFile of this.outputFiles) {
if (!outFile.target)
outFile.targetId = null;
}
}
importFromProject(other, doTriggerEvent = true) {
this.inputFiles = other.getInputFiles().map((file) => file.copy());
this.outputFiles = other.getOutputFiles().map((file) => file.copy(this));
this.configuration = structuredClone(other.getConfiguration());
if (doTriggerEvent)
this.emitEvents('inputFiles', 'outputFiles', 'configuration');
}
emitEvents(...events) {
for (const event of events)
this.batchedEvents.add(event);
// Do not emit when empty
if (!this.batchedEvents.size)
return;
// Do not emit events when batching
if (this.batchCounter > 0)
return;
// Emit new + batched events
if (this.eventCallback)
this.eventCallback(this, Array.from(this.batchedEvents));
this.batchedEvents.clear();
}
batchEvents(func, ...events) {
this.batchCounter += 1;
const res = func();
this.batchCounter -= 1;
this.emitEvents(...events);
return res;
}
static emitsEvents(...events) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function decorator(_target, _propertyKey, descriptor) {
const originalMethod = descriptor.value;
if (!originalMethod)
throw new Error('No original method!');
descriptor.value = function (...args) {
return this.batchEvents(() => originalMethod.apply(this, args), ...events);
};
return descriptor;
};
}
static serialize(project) {
return {
name: project.name,
inputFiles: project.inputFiles.map((file) => file.serialize()),
outputFiles: project.outputFiles.map((file) => file.serialize()),
configuration: project.configuration
};
}
static deserialize(data, ..._args) {
const name = data.name;
const inputFiles = data.inputFiles ?? [];
const outputFiles = data.outputFiles ?? [];
const configuration = data.configuration ?? {};
return new Project(name, inputFiles, outputFiles, configuration);
}
static loadFromData(rawData) {
const data = (0, util_js_1.decodeJSON)(rawData);
const project = Project.deserialize(data);
return project;
}
static storeToData(project) {
const data = Project.serialize(project);
return (0, util_js_1.encodeJSON)(data, true);
}
}
exports.Project = Project;
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "addInputFiles", null);
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "removeInputFiles", null);
__decorate([
Project.emitsEvents('outputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "addOutputFiles", null);
__decorate([
Project.emitsEvents('outputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array]),
__metadata("design:returntype", void 0)
], Project.prototype, "removeOutputFiles", null);
__decorate([
Project.emitsEvents(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], Project.prototype, "expireOutputFiles", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setTopLevelModule", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", void 0)
], Project.prototype, "setTestbenchPath", null);
__decorate([
Project.emitsEvents('inputFiles'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "setInputFileType", null);
__decorate([
Project.emitsEvents('configuration'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "updateConfiguration", null);
__decorate([
Project.emitsEvents(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Project, Object]),
__metadata("design:returntype", void 0)
], Project.prototype, "importFromProject", null);
//# sourceMappingURL=project.js.map