Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ jobs:
matrix:
#node-version: ['latest', '22.x', '20.x']
node-version: ['22.x', '20.x']
os: [ubuntu-latest, windows-latest, macos-latest]
# node-gyp not working on the lastest windows (2025), needs to be updated
os: [ubuntu-latest, windows-2022, macos-latest]
Comment on lines +139 to +140

runs-on: ${{ matrix.os }}

Expand Down
4 changes: 4 additions & 0 deletions Fortran-JS/src-api/FortranJoinPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,8 @@ export default class FortranJoinPoints {
name, args.map(unwrapJoinPoint)
));
}

static parenExpr(expr: Joinpoints.Expr): Joinpoints.Expr {
return wrapJoinPoint(FortranJavaTypes.AstFactory.parenExpr(unwrapJoinPoint(expr)));
}
}
13 changes: 13 additions & 0 deletions Fortran-JS/src-api/Joinpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,19 @@ const JoinpointMapper = {
ifStatement: IfStatement,
ompBlockConstruct: OmpBlockConstruct,
arraySubscriptExpr: ArraySubscriptExpr,
// Declaration/specification node types present in the Java weaverspecs but
// without dedicated TypeScript classes. Mapped to Statement so that
// Query.search and $jp.descendants traversals do not crash when encountering
// variable type declarations (e.g. `integer :: i, j`).
typeDeclarationStatement: Statement,
specificationStatement: Statement,
entityDecl: Statement,
fortranDecl: Statement,
exprInitialization: Statement,
initialization: Statement,
attributeSpecifier: Statement,
keywordAttributeSpecifier: Statement,
parameterKeyword: Statement,
};

let registered = false;
Expand Down
64 changes: 61 additions & 3 deletions Fortran-JS/src-api/code/LoopFission.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { DoStatement, RangeLoopControl } from "../Joinpoints.js";
import Query from "@specs-feup/lara/api/weaver/Query.js";
import { ArraySubscriptExpr, AssignmentStatement, DoStatement, Joinpoint,
RangeLoopControl } from "../Joinpoints.js";

/**
* Splits a do-loop with multiple body statements into one loop per statement.
Expand Down Expand Up @@ -44,7 +46,63 @@ export default function loopFission($loop: DoStatement): DoStatement[] {
return result
}

function stmtArrayWrites(stmt: Joinpoint): Set<string> {
const result = new Set<string>();
for (const assign of Query.searchFromInclusive(stmt, AssignmentStatement)) {
const lhs = assign.variable;
if (lhs instanceof ArraySubscriptExpr) result.add(lhs.var.name);
}
return result;
}

function stmtArrayReads(stmt: Joinpoint): Set<string> {
const result = new Set<string>();
for (const assign of Query.searchFromInclusive(stmt, AssignmentStatement)) {
for (const expr of Query.searchFromInclusive(assign.expr, ArraySubscriptExpr)) {
result.add(expr.var.name);
}
}
return result;
}

function stmtScalarWrites(stmt: Joinpoint): string[] {
const result: string[] = [];
for (const assign of Query.searchFromInclusive(stmt, AssignmentStatement)) {
const lhs = assign.variable;
if (!(lhs instanceof ArraySubscriptExpr)) result.push(lhs.name);
}
return result;
}

export function canFission($loop: DoStatement): boolean {
return $loop.control instanceof RangeLoopControl
&& $loop.body.executableStmts.length > 1;
if (!($loop.control instanceof RangeLoopControl)) return false;
const stmts = [...$loop.body.executableStmts];
if (stmts.length <= 1) return false;

const arrayReads = stmts.map(s => stmtArrayReads(s));
const arrayWrites = stmts.map(s => stmtArrayWrites(s));
const scalarWrites = stmts.map(s => stmtScalarWrites(s));

for (let j = 1; j < stmts.length; j++) {
// Check 1: scalar written in an earlier stmt appears in a later stmt's code.
// After fission, the later loop sees only the scalar value from its own
// iteration — it cannot observe the value threaded from the earlier loop.
for (let i = 0; i < j; i++) {
for (const sv of scalarWrites[i]) {
if (new RegExp(`\\b${sv}\\b`).test(stmts[j].code)) return false;
}
}

// Check 2: a later stmt writes an array that an earlier stmt reads.
// After fission, the earlier loop (for ALL iterations) runs before the
// later loop, so the earlier loop reads stale values for every iteration
// after the first one that the later loop would have updated.
for (const wName of arrayWrites[j]) {
for (let i = 0; i < j; i++) {
if (arrayReads[i].has(wName)) return false;
}
}
}

return true;
}
74 changes: 74 additions & 0 deletions Fortran-JS/src-api/code/LoopInterchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Query from "@specs-feup/lara/api/weaver/Query.js";
import FortranJoinPoints from "../FortranJoinPoints.js";
import { DataRef, DoStatement, ExecutableStatement, RangeLoopControl } from "../Joinpoints.js";

/**
* Swaps the outer and inner loop of a perfect 2-deep nest.
*
* Transforms:
* ```fortran
* do i = lo_i, hi_i
* do j = lo_j, hi_j
* body
* end do
* end do
* ```
* Into:
* ```fortran
* do j = lo_j, hi_j
* do i = lo_i, hi_i
* body
* end do
* end do
* ```
*
* Returns unchanged [outer] if canInterchange() fails.
*/
export default function loopInterchange(outer: DoStatement, inner: DoStatement): DoStatement[] {
if (!canInterchange(outer, inner)) return [outer];
const oc = outer.control as RangeLoopControl;
const ic = inner.control as RangeLoopControl;

// New outer uses inner's control (deep-copied)
const newOuterCtrl = FortranJoinPoints.rangeLoopControl(
ic.var.deepCopy() as DataRef,
ic.lower.deepCopy(),
ic.upper.deepCopy()
);
if (ic.step !== undefined) newOuterCtrl.setStep(ic.step.deepCopy());
const newOuterDo = FortranJoinPoints.doStatement(newOuterCtrl);

// New inner uses outer's control (deep-copied)
const newInnerCtrl = FortranJoinPoints.rangeLoopControl(
oc.var.deepCopy() as DataRef,
oc.lower.deepCopy(),
oc.upper.deepCopy()
);
if (oc.step !== undefined) newInnerCtrl.setStep(oc.step.deepCopy());
const newInnerDo = FortranJoinPoints.doStatement(newInnerCtrl);

// Deep-copy body statements into new inner loop
for (const stmt of inner.body.executableStmts) {
newInnerDo.body.insertEnd(stmt.deepCopy() as ExecutableStatement);
}
newOuterDo.body.insertEnd(newInnerDo);
outer.replaceWith(newOuterDo);
return [newOuterDo];
}

export function canInterchange(outer: DoStatement, inner: DoStatement): boolean {
const oc = outer.control, ic = inner.control;
if (!(oc instanceof RangeLoopControl && ic instanceof RangeLoopControl)) return false;
const outerVar = oc.var.name;

// Check 1: triangular inner bounds — inner bound references outer variable
if (ic.lower.code.includes(outerVar) || ic.upper.code.includes(outerVar)) return false;
Comment on lines +64 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check inner-loop steps for outer-variable references

When the inner step references the outer iterator, such as do i=1,n; do j=1,n,i, these checks approve the nest because they inspect only the lower and upper bounds. Line 38 then moves that step to the new outer loop, where i has not yet been initialized by the new inner loop, so the transformed iteration space is undefined or different; reject references to the outer iterator in ic.step as well.

Useful? React with 👍 / 👎.


// Check 2: nested DO inside body uses outer variable in its bounds
for (const nested of Query.searchFrom(inner.body, DoStatement)) {
const nc = nested.control;
if (!(nc instanceof RangeLoopControl)) continue;
if (nc.lower.code.includes(outerVar) || nc.upper.code.includes(outerVar)) return false;
}
Comment on lines +62 to +72
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject loop-carried dependencies before interchange

Reject nests whose dependence directions make interchange illegal rather than returning true solely from bound checks. For example, do i=2,n; do j=1,n-1; a(i,j)=a(i-1,j+1)+1 is valid in the original order, but after this pass the i iterations at a fixed j read a(i-1,j+1) before the future j+1 iteration has produced it, silently changing the result. Because the new generic pass applies this transformation automatically, the body accesses need a dependence legality check before approving the pair.

Useful? React with 👍 / 👎.

}
23 changes: 21 additions & 2 deletions Fortran-JS/src-api/code/LoopTiling.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Query from "@specs-feup/lara/api/weaver/Query.js";
import FortranJoinPoints from "../FortranJoinPoints.js";
import { DataRef, DoStatement, ExecutableStatement, RangeLoopControl } from "../Joinpoints.js";

Expand Down Expand Up @@ -85,8 +86,26 @@ export default function loopTile(outer: DoStatement, inner: DoStatement, tileSiz
return [outerTileDo];
}

function containsVar(code: string, varName: string): boolean {
return new RegExp(`\\b${varName}\\b`).test(code);
}

export function canTile(outer: DoStatement, inner: DoStatement): boolean {
const oc = outer.control, ic = inner.control;
return oc instanceof RangeLoopControl && ic instanceof RangeLoopControl
&& oc.step === undefined && ic.step === undefined;
if (!(oc instanceof RangeLoopControl && ic instanceof RangeLoopControl)) return false;
if (oc.step !== undefined || ic.step !== undefined) return false;

const outerVar = oc.var.name;

// Check 1: triangular inner bounds — inner bound references outer variable
if (containsVar(ic.lower.code, outerVar) || containsVar(ic.upper.code, outerVar)) return false;

// Check 2: nested DO inside inner body uses outer variable in its bounds
for (const nested of Query.searchFrom(inner.body, DoStatement)) {
const nc = nested.control;
if (!(nc instanceof RangeLoopControl)) continue;
if (containsVar(nc.lower.code, outerVar) || containsVar(nc.upper.code, outerVar)) return false;
}

return true;
}
58 changes: 36 additions & 22 deletions Fortran-JS/src-api/code/LoopUnroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import { DataRef, DoStatement, ExecutableStatement, RangeLoopControl } from "../

/**
* Deep-copies a body statement and replaces every reference to `varName` with
* `varName+offset` using AST-level substitution (only real variable references
* `(varName+offset)` using AST-level substitution (only real variable references
* are affected, not occurrences inside string literals or comments).
*
* The replacement is wrapped in parenExpr() so that when it appears as the
* right operand of a surrounding subtraction (e.g. `n - i`) the emitted code
* is `n - (i + 1)` rather than `n - i + 1` (which Fortran evaluates as
* `n - i + 1`, flipping the sign of the offset).
*/
function substituteVar(stmt: ExecutableStatement, varName: string, offset: number): ExecutableStatement {
const copy = stmt.deepCopy() as ExecutableStatement;
if (offset === 0) return copy;
for (const ref of Query.searchFrom(copy, DataRef, { name: varName })) {
const binOp = FortranJoinPoints.binaryOperatorAdd(ref.deepCopy(), FortranJoinPoints.intLiteral(offset));
const binOp = FortranJoinPoints.parenExpr(
FortranJoinPoints.binaryOperatorAdd(ref.deepCopy(), FortranJoinPoints.intLiteral(offset))
);
ref.replaceWith(binOp)
}
return copy;
Expand All @@ -22,7 +29,7 @@ function substituteVar(stmt: ExecutableStatement, varName: string, offset: numbe
*
* Two loops replace the original:
* - A **main loop** that steps by `factor`, with the body replicated `factor`
* times (each copy substitutes `var` with `var+offset` for offset 0..factor-1).
* times (each copy substitutes `var` with `(var+offset)` for offset 0..factor-1).
* - A **cleanup loop** that handles the remaining < `factor` iterations with the
* original body. When `factor` exactly divides the trip count, the cleanup
* loop's bounds produce a no-op and it generates no iterations.
Expand All @@ -37,10 +44,10 @@ function substituteVar(stmt: ExecutableStatement, varName: string, offset: numbe
* // end do
*
* // After:
* // do i = 1, (n) - 3, 4
* // a(i) = b(i); a(i+1) = b(i+1); a(i+2) = b(i+2); a(i+3) = b(i+3)
* // do i = 1, n - 3, 4
* // a(i) = b(i); a((i+1)) = b((i+1)); a((i+2)) = b((i+2)); a((i+3)) = b((i+3))
* // end do
* // do i = (1) + (((n) - (1) + 1) / 4) * 4, n
* // do i = n + 1 - MOD(n - (1) + 1, 4), n
* // a(i) = b(i)
* // end do
*
Expand All @@ -59,7 +66,7 @@ export default function loopUnroll($loop: DoStatement, factor: number): DoStatem
const mainCtrl = mainDo.control as RangeLoopControl;

mainCtrl.setUpper(FortranJoinPoints.binaryOperatorSubtract(
ctrl.upper.deepCopy(),
ctrl.upper.deepCopy(),
FortranJoinPoints.intLiteral(factor - 1))
);
mainCtrl.setStep(FortranJoinPoints.intLiteral(factor));
Expand All @@ -70,21 +77,28 @@ export default function loopUnroll($loop: DoStatement, factor: number): DoStatem
}
}

// Cleanup loop: lo + ((hi - lo + 1) / factor) * factor .. hi
// Integer division makes this a no-op when factor exactly divides the trip count.
const cleanupLower = FortranJoinPoints.binaryOperatorAdd(
ctrl.lower.deepCopy(),
FortranJoinPoints.binaryOperatorMultiply(
FortranJoinPoints.binaryOperatorDivide(
FortranJoinPoints.binaryOperatorAdd(
FortranJoinPoints.binaryOperatorSubtract(ctrl.upper.deepCopy(), ctrl.lower.deepCopy()),
FortranJoinPoints.intLiteral(1)
),
FortranJoinPoints.intLiteral(factor)
),
FortranJoinPoints.intLiteral(factor)
)
// Cleanup loop: hi + 1 - MOD(hi - (lo) + 1, factor) .. hi
//
// lo is wrapped in parenExpr() so compound lower bounds like (k+1) or (i+1)
// emit as `hi - (k+1) + 1 = hi - k` instead of `hi - k + 1 + 1 = hi - k + 2`.
// MOD's argument list is syntactically parenthesized, so the full tripCount
// expression inside it is evaluated correctly regardless of complexity.
const tripCount = FortranJoinPoints.binaryOperatorAdd(
FortranJoinPoints.binaryOperatorSubtract(
ctrl.upper.deepCopy(),
FortranJoinPoints.parenExpr(ctrl.lower.deepCopy())
),
FortranJoinPoints.intLiteral(1)
);
const remainder = FortranJoinPoints.intrinsicCall("MOD", [
tripCount,
FortranJoinPoints.intLiteral(factor)
]);
const cleanupLower = FortranJoinPoints.binaryOperatorSubtract(
FortranJoinPoints.binaryOperatorAdd(ctrl.upper.deepCopy(), FortranJoinPoints.intLiteral(1)),
remainder
);

const cleanupCtrl = FortranJoinPoints.rangeLoopControl(ctrl.var.deepCopy() as DataRef, cleanupLower, ctrl.upper.deepCopy());
const cleanupDo = FortranJoinPoints.doStatement(cleanupCtrl);
for (const stmt of bodyStmts) {
Expand All @@ -99,4 +113,4 @@ export function canUnroll($loop: DoStatement, factor: number = 2): boolean {
if (!($loop.control instanceof RangeLoopControl)) return false;
if (factor <= 1) return false;
return ($loop.control as RangeLoopControl).step === undefined;
}
}
18 changes: 18 additions & 0 deletions Fortran-JS/src-api/examples/fissionGeneric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Query from "@specs-feup/lara/api/weaver/Query.js";
import LoopFissionPass from "../pass/LoopFissionPass.js";
import { Subroutine } from "../Joinpoints.js";

const subroutines = Query.search(Subroutine, ($jp) => $jp.moduleName.startsWith('kernel_')).get();

if (subroutines.length === 0) {
console.log('No kernel_* subroutine found — skipping');
} else {
for (const sub of subroutines) {
const result = new LoopFissionPass().apply(sub);
if (result.appliedPass) {
console.log(`[fissionGeneric] FISSIONED: ${sub.moduleName}`);
} else {
console.log(`[fissionGeneric] SKIPPED (no eligible multi-statement loops): ${sub.moduleName}`);
}
}
}
18 changes: 18 additions & 0 deletions Fortran-JS/src-api/examples/fusionGeneric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Query from "@specs-feup/lara/api/weaver/Query.js";
import LoopFusionPass from "../pass/LoopFusionPass.js";
import { Subroutine } from "../Joinpoints.js";

const subroutines = Query.search(Subroutine, ($jp) => $jp.moduleName.startsWith('kernel_')).get();

if (subroutines.length === 0) {
console.log('No kernel_* subroutine found — skipping');
} else {
for (const sub of subroutines) {
const result = new LoopFusionPass().apply(sub);
if (result.appliedPass) {
console.log(`[fusionGeneric] FUSED: ${sub.moduleName}`);
} else {
console.log(`[fusionGeneric] SKIPPED (no fusable consecutive loops): ${sub.moduleName}`);
}
}
}
18 changes: 18 additions & 0 deletions Fortran-JS/src-api/examples/interchangeGeneric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Query from "@specs-feup/lara/api/weaver/Query.js";
import LoopInterchangePass from "../pass/LoopInterchangePass.js";
import { Subroutine } from "../Joinpoints.js";

const subroutines = Query.search(Subroutine, ($jp) => $jp.moduleName.startsWith('kernel_')).get();

if (subroutines.length === 0) {
console.log('No kernel_* subroutine found — skipping');
} else {
for (const sub of subroutines) {
const result = new LoopInterchangePass().apply(sub);
if (result.appliedPass) {
console.log(`[interchangeGeneric] INTERCHANGED: ${sub.moduleName}`);
} else {
console.log(`[interchangeGeneric] SKIPPED (no eligible/legal 2-deep perfect nest): ${sub.moduleName}`);
}
}
}
Loading
Loading