Skip to content

fix(tests): quote the paths in runE2ETestPipeline's g++ command - #226

Open
MatthewReed303 wants to merge 7 commits into
Autonomy-Logic:developmentfrom
MatthewReed303:fix/test-helpers-quote-paths
Open

fix(tests): quote the paths in runE2ETestPipeline's g++ command#226
MatthewReed303 wants to merge 7 commits into
Autonomy-Logic:developmentfrom
MatthewReed303:fix/test-helpers-quote-paths

Conversation

@MatthewReed303

Copy link
Copy Markdown
  • runE2ETestPipeline built its g++ command by join(' ')-ing an argv array into
    a shell string and quoted nothing, so any repo path containing a space
    word-split the -I flags and backgrounded the command at an &.
  • 115 tests across 7 integration files failed for this reason alone on a checkout
    under such a path.
  • Quoted in the same style the neighbouring call sites in this file already use
    (:93, :156, and oscat-gpp-compile.test.ts:185-193). No behaviour change
    where the path has no spaces.

The argv array was joined into a shell string with nothing quoted, so a
repo path containing a space word-split the -I flags and 115 tests failed.
Quoted in the style the neighbouring call sites already use.
…h the runtime

An imported CODESYS library using ANY did not compile: generics existed
nowhere. They are now declarable on a VAR_INPUT of a FUNCTION, FUNCTION_BLOCK
or METHOD — CODESYS's seven names, since ANY_ELEMENTARY, ANY_MAGNITUDE and
ANY_DERIVED classify types without being declarable — and never as an array
element. iec_any.hpp carries the TYPE_CLASS enumeration and the IEC_ANY
descriptor; __SYSTEM.AnyType parses as a qualified type name, gated on that
namespace so a dot after any other type stays an error, and is declarable in
its own right so a block can keep what it was handed.

The analyzer enforces CODESYS's two rules: only a variable may be passed, a
literal or expression having no address; and its type must be one the declared
family accepts. That check earns its place — a concrete parameter is caught by
C++ refusing the assignment, but a REAL handed to an ANY_INT still produces
valid C++, a descriptor stamped TYPE_REAL the block was never written for.

Codegen builds the descriptor at every call shape: function block calls named
and positional, both AST shapes a method call arrives in, and FUNCTION calls.
typeclass comes from the declared type, because the payload cannot tell BYTE
from USINT; pvalue from raw_ptr(); diSize from IEC_SIZEOF. A STRING passed
this way queues a sync_length() flushed after the statement, since the callee
writes the characters but not the cached length. arr[i] and s.field reach a
generic pin too — they are VariableExpressions carrying subscripts, so they
passed the variable guard and then failed a lookup keyed on the variable's own
name.

ARRAY [*] OF T as a VAR_IN_OUT comes with it. The parameter is an ArrayView
carrying the runtime bounds, so it needs no copy back and cannot have one, and
ArrayView gains an unbound default because a function block stores its in-outs
as members before there is an array to point at.

STRING(23) parsed and was then widened to 254 downstream. An inline array
dropped its element's length; a VAR_EXTERNAL widened through toParamTypeRef,
which a parameter may do and a GlobalVar<…>* may not. TypeReference carries
elementMaxLength through the project model, and the three external sites share
one externalTypeRefCpp helper.

The debug dispatch was worse than oversized: its four string ops hard-cast
every string to IECStringVar<254>, so a STRING(23) read its length from past
the end of a 54-byte object and a write constructed 258 bytes into it. The
spare byte in Entry becomes cap, the ops take it, and IECStringView locates
each field from the capacity, with static_asserts pinning the layout at 1, 23
and 254. The table also walks the EXTENDS chain now, so an inherited member is
watchable, carrying the declaring type so memberCppName spells it right.

Three conformance fixes. REAL_TO_INT(2.5) answered 3, because
iec_convert_value used std::round, which rounds halves away from zero;
ties-to-even is written out with floor and fmod, which avr-libc carries.
INSERT('ABC','XY',2) answered 'AXYBC' — it inserted before the P-th character
rather than after it, while DELETE_STR beside it is right to keep pos - 1.
CONTINUE had no token and no parser rule.

Also: raw_ptr() and sync_length() on both string classes so a generic
parameter or a driver reaches the characters and not the forcing wrapper;
force() writes the raw value too, so a forced variable reads as forced through
a descriptor; IEC_SIZEOF reports capacity + 1 rather than the wrapper's size;
IECVar::value_field_offset() with static_asserts that the payload stays first,
since ADR(x) lowers to &(x); a native block can name a structure, enumeration
or function block its own library declares, which only worked across a real
dependency before; and a library's enum members are exported, so a bare
enumerator from one qualifies instead of failing to compile.
…ccess,

and generics that carry a composite

Forcing. IECStringVar, IECWStringVar, IEC_ENUM_Var and IEC_SUBRANGE_Var each
re-declared the force members but not the behaviour: set() was unguarded where
IECVar's is guarded, force() did not mirror into value_, and copy assignment
was memberwise. The debug layer already treated all four as IECVar by casting
to it, so the same variable behaved differently depending on whether ST or the
debugger touched it. All four now match IECVar exactly, and static_asserts pin
the member offsets the AVR read path depends on.

Both AVR branches of read_entry skipped Entry::cap, so every sized STRING read
as the 254 default and the string ops computed their forced-value offsets past
the end of the object. cap is now read at sizeof(void*) + 1.

test_iec_composite.cpp — 928 lines holding the only cover for array-element,
enum, subrange and struct-field forcing — was in no CMakeLists and would not
compile. Repaired and registered.

Long time types. LTIME, LTOD and LDT are registered with their literal
prefixes and the one-directional promotions from their short forms. The
literals lexed and then silently produced 0, because the DATE/TOD/DT
converters strip their own prefixes and were never taught the long forms.
LDATE stays out: it needs nanoseconds where DATE_t holds whole days, and its
debug-table mapping is removed rather than left wrong by 86400e9.

Partial access. .%X, .%B, .%W and .%D are lexed, parsed, type-resolved,
bounds-checked and lowered, for reads and writes alike. Index 0 is the least
significant part, a write is a read-modify-write masked to the part's width,
and all of it is 64-bit so a shift of 32 is defined. One parsePartialAccess is
shared by the six places that used to test /^\d+$/ on a field, so a bare var.3
and var.%X3 lower identically. A part of an integer is accepted and warns.

Unknown calls. compile("i := ZZQQ(1)") returned success: an unknown call was
emitted verbatim and failed at g++, or was rescued by C++ name lookup. Now
reported, resolving own methods through the EXTENDS chain and both SUPER^
forms. It immediately found TIME_TO_NS/US/M/H/D and the date-time arithmetic
family implemented in the runtime and never registered.

Generics. A generic pin now carries an array, a structure or an enumeration.
The descriptor gains DICOUNT and DISTRIDE so a block can walk what it was
handed, and its members are zeroed so an unwired pin reads as nothing rather
than as whatever was on the stack. IEC_SIZEOF reports the data a
variable-length array view addresses rather than the view's own size, which is
what makes SIZEOF(a) / count the element stride on any target. POINTER TO
lowers to IEC_Ptr in a structure element and a type alias, where it used to
append a star to the wrapped type and fail in C++ rather than here.

2560 tests passing; the 7 failures in integer-literal-exact-cpp.test.ts are
the pre-existing Clang-only ones. C++ runtime 156/156.
VAR_IN_OUT was barely checked: an argument could be a literal, a constant,
or absent altogether, and the copy-back then emitted `7 = A.VAL`. A new
analyzer pass covers every in-out of every call and resolves the callee
through the symbol table, so it sees library blocks — the old check read
the AST and never fired for them.

A block passed as an in-out is a pointer at the caller's instance rather
than a copy, so it can be driven and its outputs read but not written.
Arguments given without a name fill the slots the named ones left, in
declaration order; codegen dropped them before.

A structure holding a block instance is emitted after the class it holds,
and a call written as a dotted path drives the instance instead of looking
for a method of that name. Together those let two programs share one
instance through a global.

IEC_ANY gains ELEMCLASS, so an array on a generic pin carries its element
type instead of leaving the block to infer it from byte arithmetic.
The debugger locates a string's fields from a void* and a capacity, so the
offsets are predicted arithmetically and checked against the real layout. The
prediction assumed a 16-bit load needs an even address and rounded the length
up to a multiple of two.

AVR byte-aligns every type, so there the length sits immediately after the
characters and the prediction pointed two bytes past it. Only an even capacity
shows it: STRING(23) rounds to the same place either way, while the unqualified
IEC_STRING is IECStringVar<254> and does not. The forced value had the same
fault a second time, adding the padding a bool needs on a 2-aligned target and
that AVR does not.

Both now round to alignof(IECString<1>). The alignment does not vary with the
capacity, so one instantiation answers for every one, and a wider member added
later moves the offsets with it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant