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
248 changes: 206 additions & 42 deletions src/backend/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
isImplicitlyConvertible,
resolveFieldType as resolveFieldTypeUtil,
resolveArrayElementType as resolveArrayElementTypeUtil,
resolveArrayShapeByName,
typeName as typeNameUtil,
buildEnumMemberMap,
type EnumMemberEntry,
Expand All @@ -95,6 +96,17 @@ interface LocatedVarDescriptor {
bitIndex: number;
typeName: string;
programName: string;
/**
* IEC index of the array element this descriptor binds, for a located
* ARRAY. Absent for a scalar.
*
* A located array is emitted as one descriptor PER ELEMENT, laid out over
* consecutive addresses -- `AT %MW60 : ARRAY [0..66] OF WORD` becomes 67
* descriptors at %MW60..%MW126. The descriptor table is flat and carries no
* notion of an aggregate, so the element index is what tells the pointer
* initialiser to bind `arr[i]` rather than `arr` (openplc-editor#565).
*/
elementIndex?: number;
}

/**
Expand Down Expand Up @@ -2688,7 +2700,14 @@ export class CodeGenerator {
// (not a real program) keeps it out of every program's located_range.
if (gvar.address) {
this.collectLocatedVarFromModel(
{ name: gvar.name, typeName: gvar.typeName, address: gvar.address },
{
name: gvar.name,
typeName: gvar.typeName,
address: gvar.address,
// Carried through so a located ARRAY global expands to one
// descriptor per element rather than binding only its first.
arrayDimensions: gvar.arrayDimensions,
},
"@config",
);
}
Expand Down Expand Up @@ -5689,6 +5708,102 @@ export class CodeGenerator {
return count;
}

/**
* Push the descriptor(s) one located declaration produces.
*
* A scalar produces one. An ARRAY produces one PER ELEMENT, walking the
* address forward by one slot each time, so `AT %MW60 : ARRAY [0..66] OF
* WORD` fills %MW60..%MW126 (openplc-editor#565). The runtime table is flat
* and knows nothing about aggregates -- the expansion happens here so that
* every consumer of `locatedVars[]` (the descriptor array, the count, the
* per-program range, the located-globals list) gets the element-level view
* without any of them having to understand arrays.
*
* Bit addresses advance across the byte boundary (%IX0.7 -> %IX1.0), which
* is why the step is computed on the linearised index rather than on
* `byteIndex` alone.
*
* `dims` is `undefined` for a non-array. The semantic analyzer has already
* rejected the array shapes that cannot be laid out linearly (multi-
* dimensional, non-constant bounds), so anything reaching here with bounds
* is a single dimension with a known extent.
*/
/**
* The dimensions a located declaration actually has, resolving a NAMED
* ARRAY type the way the semantic analyzer already does.
*
* The AST builder writes bounds onto the TypeReference only for an INLINE
* `ARRAY [a..b] OF T`. A named type (`TYPE Buf : ARRAY [0..9] OF WORD`) has
* none, so reading `decl.type.arrayDimensions` alone made codegen see a
* scalar where the analyzer had seen ten slots: one descriptor was emitted
* and the pointer initialiser called `.raw_ptr()` on the array itself, which
* does not compile. Resolving by name here keeps the two passes agreeing.
*
* Answers `undefined` for anything that is not a single fixed dimension --
* multi-dimensional and variable-length shapes are rejected in semantics and
* never reach codegen, so this is a fallback rather than a second opinion.
*/
private resolveLocatedDims(
typeName: string,
dims: Array<{ start: number; end: number }> | undefined,
): Array<{ start: number; end: number }> | undefined {
if (dims && dims.length > 0) return dims;
if (!this.ast) return undefined;

const shape = resolveArrayShapeByName(typeName, this.ast);
if (!shape || shape.dims.length !== 1) return undefined;

const dim = shape.dims[0];
return dim ? [dim] : undefined;
}

private pushLocatedDescriptors(
varName: string,
address: string,
typeName: string,
programName: string,
dims: Array<{ start: number; end: number }> | undefined,
): void {
const parsed = parseLocatedAddress(address);
if (!parsed) return;

const resolved = this.resolveLocatedDims(typeName, dims);
const dim = resolved?.length === 1 ? resolved[0] : undefined;
if (!dim) {
this.locatedVars.push({
varName,
address,
area: parsed.area,
size: parsed.size,
byteIndex: parsed.byteIndex,
bitIndex: parsed.bitIndex,
typeName,
programName,
});
return;
}

const isBit = parsed.size === "Bit";
const baseSlot = isBit
? parsed.byteIndex * 8 + parsed.bitIndex
: parsed.byteIndex;

for (let iecIndex = dim.start; iecIndex <= dim.end; iecIndex++) {
const slot = baseSlot + (iecIndex - dim.start);
this.locatedVars.push({
varName,
address,
area: parsed.area,
size: parsed.size,
byteIndex: isBit ? Math.floor(slot / 8) : slot,
bitIndex: isBit ? slot % 8 : 0,
typeName,
programName,
elementIndex: iecIndex,
});
}
}

/**
* Collect a located variable for descriptor array generation.
*/
Expand All @@ -5699,43 +5814,41 @@ export class CodeGenerator {
): void {
if (!decl.address) return;

const parsed = parseLocatedAddress(decl.address);
if (!parsed) return;

this.locatedVars.push({
this.pushLocatedDescriptors(
varName,
address: decl.address,
area: parsed.area,
size: parsed.size,
byteIndex: parsed.byteIndex,
bitIndex: parsed.bitIndex,
typeName: decl.type.name,
decl.address,
decl.type.name,
programName,
});
// Inline `ARRAY [a..b] OF T`: the AST builder resolves the bounds onto
// the TypeReference. A named ARRAY type carries none here and is
// handled by the model path, which resolves the alias first.
decl.type.arrayDimensions,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

HIGH — regression: named ARRAY types reach codegen and fail there.

collectLocatedVar passes decl.type.arrayDimensions, which is undefined for a named ARRAY type, while the analyzer's resolveLocatedShape resolves named types through resolveArrayShapeByName. The two disagree.

TYPE Buf : ARRAY [0..9] OF WORD; END_TYPE
VAR_GLOBAL HR AT %MW60 : Buf; END_VAR

This passes semantics and correctly reserves 10 slots (a second variable at %MW65 is properly rejected), but codegen emits one descriptor and calls HR.raw_ptr() on an array:

error: no member named 'raw_ptr' in 'strucpp::IEC_ARRAY_1D<strucpp::IECVar<unsigned short>, strucpp::ArrayBounds<0, 9>>'

On development the analyzer rejected this cleanly with Type 'Buf' is not compatible with address size 'W'. It now fails in the C++ backend leaking internal type names — the exact failure mode this PR set out to remove.

The PR's own test accepts the same shape spelled as a named ARRAY type only asserts compile(source).success and has no CONFIGURATION, so codegen's located path is never exercised. Suggest resolving the named type here the same way resolveLocatedShape does, and extending that test with a CONFIGURATION block.

);
}

/**
* Collect a located variable from project model for descriptor array generation.
*/
private collectLocatedVarFromModel(
decl: { name: string; typeName: string; address?: string },
decl: {
name: string;
typeName: string;
address?: string;
// Explicit `| undefined` (not just `?`): exactOptionalPropertyTypes is
// on, and callers forward an optional field straight through.
arrayDimensions?: Array<{ start: number; end: number }> | undefined;
},
programName: string,
): void {
if (!decl.address) return;

const parsed = parseLocatedAddress(decl.address);
if (!parsed) return;

this.locatedVars.push({
varName: decl.name,
address: decl.address,
area: parsed.area,
size: parsed.size,
byteIndex: parsed.byteIndex,
bitIndex: parsed.bitIndex,
typeName: decl.typeName,
this.pushLocatedDescriptors(
decl.name,
decl.address,
decl.typeName,
programName,
});
decl.arrayDimensions,
);
}

/**
Expand Down Expand Up @@ -5775,15 +5888,21 @@ export class CodeGenerator {
this.emitHeader(" */");
this.emitHeader("");

// Forward declarations for program instances
// Forward declarations for program instances.
//
// One line per DECLARATION, not per descriptor: a located array expands to
// one descriptor per element, and repeating the same "AT %MW60" line 67
// times would bury the rest of the header in noise.
const listed = new Set<string>();
for (const locVar of this.locatedVars) {
const scope =
locVar.programName === "@config"
? "configuration"
: `Program_${locVar.programName}`;
this.emitHeader(
`// Forward: ${locVar.varName} AT ${locVar.address} in ${scope}`,
);
const line = `// Forward: ${locVar.varName} AT ${locVar.address} in ${scope}`;
if (listed.has(line)) continue;
listed.add(line);
this.emitHeader(line);
}
if (isEmpty) {
this.emitHeader("// (no located variables — placeholder entry only)");
Expand Down Expand Up @@ -5844,7 +5963,7 @@ export class CodeGenerator {
const comma = i < this.locatedVars.length - 1 ? "," : "";
this.emit(
` { LocatedArea::${locVar.area}, LocatedSize::${locVar.size}, ` +
`${locVar.byteIndex}, ${locVar.bitIndex}, {0, 0, 0}, nullptr }${comma} // ${locVar.varName} AT ${locVar.address}`,
`${locVar.byteIndex}, ${locVar.bitIndex}, {0, 0, 0}, nullptr }${comma} // ${this.describeLocatedDescriptor(locVar)}`,
);
}
}
Expand Down Expand Up @@ -5904,7 +6023,9 @@ export class CodeGenerator {
for (let i = 0; i < globals.length; i++) {
const g = globals[i]!;
const comma = i < globals.length - 1 ? "," : "";
this.emit(` nullptr${comma} // ${g.varName} AT ${g.address}`);
this.emit(
` nullptr${comma} // ${this.describeLocatedDescriptor(g)}`,
);
}
}
this.emit("};");
Expand Down Expand Up @@ -5965,17 +6086,17 @@ export class CodeGenerator {
if (progVars.length === 0) return;

this.emit(`${indent}// Initialize located variable pointers`);
for (const locVar of progVars) {
// Find the index of this variable in the global array
const index = this.locatedVars.findIndex(
(v) =>
v.varName === locVar.varName && v.programName === locVar.programName,
// Walk the global array by position rather than looking each entry back
// up by name. A located ARRAY contributes one descriptor per element, all
// sharing a varName, so a name lookup (`findIndex`) resolves every one of
// them to the FIRST slot: element 0 would be bound N times and elements
// 1..N-1 left null (openplc-editor#565). The index is right here anyway.
for (let index = 0; index < this.locatedVars.length; index++) {
const locVar = this.locatedVars[index]!;
if (locVar.programName !== programName) continue;
this.emit(
`${indent}locatedVars[${index}].pointer = ${this.locatedStorageExpr(locVar, memberAccess)};`,
);
if (index >= 0) {
this.emit(
`${indent}locatedVars[${index}].pointer = ${locVar.varName}${memberAccess}.raw_ptr();`,
);
}
}

// Configuration VAR_GLOBALs additionally record their storage pointer in
Expand All @@ -5990,13 +6111,56 @@ export class CodeGenerator {
for (let g = 0; g < progVars.length; g++) {
const locVar = progVars[g]!;
this.emit(
`${indent}locatedGlobals[${g}] = ${locVar.varName}${memberAccess}.raw_ptr();`,
`${indent}locatedGlobals[${g}] = ${this.locatedStorageExpr(locVar, memberAccess)};`,
);
}
this.emit("#endif");
}
}

/**
* The C++ expression yielding the storage a descriptor binds to.
*
* Scalar: `name[.value].raw_ptr()`. Array element: `name[.value][i].raw_ptr()`,
* where `i` is the IEC index — `IEC_ARRAY_1D::operator[]` maps the declared
* index range onto its internal storage, so the declared index is what goes
* in, not a zero-based offset.
*/
private locatedStorageExpr(
locVar: LocatedVarDescriptor,
memberAccess: string,
): string {
const element =
locVar.elementIndex === undefined ? "" : `[${locVar.elementIndex}]`;
return `${locVar.varName}${memberAccess}${element}.raw_ptr()`;
}

/**
* How a descriptor labels itself in the generated table's trailing comment.
*
* For an array element this is the address the element actually occupies,
* not the array's declared base — 67 rows all reading `AT %MW60` would tell
* a reader nothing about which slot each row binds.
*/
private describeLocatedDescriptor(locVar: LocatedVarDescriptor): string {
if (locVar.elementIndex === undefined) {
return `${locVar.varName} AT ${locVar.address}`;
}
const areaChar = { Input: "I", Output: "Q", Memory: "M" }[locVar.area];
const sizeChar = {
Bit: "X",
Byte: "B",
Word: "W",
DWord: "D",
LWord: "L",
}[locVar.size];
const offset =
locVar.size === "Bit"
? `${locVar.byteIndex}.${locVar.bitIndex}`
: `${locVar.byteIndex}`;
return `${locVar.varName}[${locVar.elementIndex}] AT %${areaChar}${sizeChar}${offset}`;
}

/**
* Emit a line to the implementation output.
*/
Expand Down
Loading