Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,42 @@ jobs:
name: strucpp-win32-x64
path: strucpp-win32-x64.zip

# ---------------------------------------------------------------------------
# npm tarball (platform-independent — used by OpenPLC Editor)
# ---------------------------------------------------------------------------
build-npm:
needs: prepare
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Inject version
run: npm version "$VERSION" --no-git-tag-version --allow-same-version

- name: Build
run: npm run build

- name: Create npm tarball
run: |
npm pack
mv strucpp-*.tgz "strucpp-${VERSION}.tgz"

- uses: actions/upload-artifact@v4
with:
name: strucpp-npm
path: strucpp-${{ needs.prepare.outputs.version }}.tgz

# ---------------------------------------------------------------------------
# VSCode Extension (.vsix)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -331,6 +367,7 @@ jobs:
- build-darwin-x64
- build-darwin-arm64
- build-windows-x64
- build-npm
- build-vsix
runs-on: ubuntu-latest
steps:
Expand All @@ -350,6 +387,8 @@ jobs:
cp artifacts/strucpp-win32-x64/strucpp-win32-x64.zip release/
# Windows ARM64 runs x64 binaries via emulation
cp release/strucpp-win32-x64.zip release/strucpp-win32-arm64.zip
# npm tarball (platform-independent, used by OpenPLC Editor)
cp artifacts/strucpp-npm/strucpp-*.tgz release/
cp artifacts/strucpp-vsix/*.vsix release/
ls -lh release/

Expand Down
128 changes: 126 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,16 @@ import { SymbolTables } from "./semantic/symbol-table.js";
import { SemanticAnalyzer } from "./semantic/analyzer.js";
import { CodeGenerator } from "./backend/codegen.js";
import { StdFunctionRegistry } from "./semantic/std-function-registry.js";
import type { CompilationUnit } from "./frontend/ast.js";
import type {
CompilationUnit,
FunctionCallExpression,
FunctionBlockDeclaration,
InterfaceDeclaration,
TypeReference,
ASTNode,
} from "./frontend/ast.js";
import { mergeCompilationUnits } from "./merge.js";
import { walkAST } from "./ast-utils.js";
import {
registerLibrarySymbols,
discoverStlibs,
Expand All @@ -41,6 +49,110 @@ export const defaultOptions: CompileOptions = {
optimizationLevel: 0,
};

// ---------------------------------------------------------------------------
// Library tree-shaking: only include libraries whose symbols are referenced
// ---------------------------------------------------------------------------

/**
* Determine which libraries are actually used by the user program.
* Walks the AST to collect all referenced symbol names, then matches them
* against library manifests to find which libraries are needed.
* Includes transitive dependencies.
*/
function collectUsedLibraries(
ast: CompilationUnit,
archives: StlibArchive[],
): Set<string> {
// Build symbol→library name map (uppercase keys for case-insensitive match)
const symbolToLib = new Map<string, string>();
for (const archive of archives) {
const libName = archive.manifest.name;
for (const fn of archive.manifest.functions) {
symbolToLib.set(fn.name.toUpperCase(), libName);
}
for (const fb of archive.manifest.functionBlocks) {
symbolToLib.set(fb.name.toUpperCase(), libName);
}
for (const t of archive.manifest.types) {
symbolToLib.set(t.name.toUpperCase(), libName);
}
}

// Collect all symbol names referenced in the AST
const referencedNames = new Set<string>();

walkAST(ast, (node: ASTNode): void => {
switch (node.kind) {
case "FunctionCallExpression": {
const fc = node as FunctionCallExpression;
referencedNames.add(fc.functionName.toUpperCase());
break;
}
case "TypeReference": {
const tr = node as TypeReference;
referencedNames.add(tr.name.toUpperCase());
if (tr.elementTypeName) {
referencedNames.add(tr.elementTypeName.toUpperCase());
}
break;
}
case "FunctionBlockDeclaration": {
const fbd = node as FunctionBlockDeclaration;
if (fbd.extends) referencedNames.add(fbd.extends.toUpperCase());
if (fbd.implements) {
for (const iface of fbd.implements) {
referencedNames.add(iface.toUpperCase());
}
}
break;
}
case "InterfaceDeclaration": {
const id = node as InterfaceDeclaration;
if (id.extends) {
for (const base of id.extends) {
referencedNames.add(base.toUpperCase());
}
}
break;
}
}
});

// Match referenced names to libraries
const usedLibs = new Set<string>();
for (const name of referencedNames) {
const lib = symbolToLib.get(name);
if (lib) usedLibs.add(lib);
}

// Add transitive dependencies
const depMap = new Map<string, string[]>();
for (const archive of archives) {
depMap.set(
archive.manifest.name,
archive.dependencies.map((d) => d.name),
);
}

let changed = true;
while (changed) {
changed = false;
for (const lib of usedLibs) {
const deps = depMap.get(lib);
if (deps) {
for (const dep of deps) {
if (!usedLibs.has(dep)) {
usedLibs.add(dep);
changed = true;
}
}
}
}
}

return usedLibs;
}

// ---------------------------------------------------------------------------
// Shared pipeline
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -476,9 +588,21 @@ export function compile(

// Register all library metadata (FB types, field mappings, enum/struct types)
codegen.registerLibraryArchives(pipeline.allArchives);

// Tree-shake: only include libraries whose symbols are referenced by the
// program. Skip for test builds — the test harness (test_main.cpp) may
// reference library symbols that aren't in the source AST.
const isTestBuild = pipeline.mergedOptions.isTestBuild ?? false;
const usedLibs = isTestBuild
? null
: collectUsedLibraries(pipeline.ast, pipeline.allArchives);

// Inject compiled C++ preamble code from libraries
for (const archive of pipeline.allArchives) {
if (archive.headerCode) {
if (
archive.headerCode &&
(usedLibs === null || usedLibs.has(archive.manifest.name))
) {
codegen.addLibraryPreamble(
archive.manifest.name,
archive.headerCode,
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/oscat-gpp-compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ describe.skipIf(!hasGpp || !oscatStlibAvailable)(
it(
"transpiles OSCAT library to C++ via .stlib",
() => {
// Compile a minimal dummy program with the OSCAT library loaded
// Compile a minimal dummy program with the OSCAT library loaded.
// isTestBuild disables library tree-shaking so all library code is emitted.
const dummyST = "PROGRAM _Dummy\nEND_PROGRAM\n";
const result = compile(dummyST, {
headerFileName: "oscat_all.hpp",
fileName: "_dummy.st",
libraryPaths: [LIBS_DIR],
isTestBuild: true,
});

if (!result.success) {
Expand Down Expand Up @@ -154,6 +156,7 @@ describe.skipIf(!hasGpp || !oscatStlibAvailable)(
headerFileName: "oscat_all.hpp",
fileName: "_dummy.st",
libraryPaths: [LIBS_DIR],
isTestBuild: true,
});
const { pous } = dummyResult.ast
? buildPOUInfoFromAST(dummyResult.ast)
Expand Down
Loading
Loading