Skip to content

TTCN-3 test-execution runtime (interpreter + ntt exec) and ETSI conformance gate#776

Open
rafael2knokia wants to merge 303 commits into
masterfrom
ntt-titan
Open

TTCN-3 test-execution runtime (interpreter + ntt exec) and ETSI conformance gate#776
rafael2knokia wants to merge 303 commits into
masterfrom
ntt-titan

Conversation

@rafael2knokia

Copy link
Copy Markdown
Collaborator

Summary

This branch grows ntt from a TTCN-3 front-end (parser / LSP / tooling)
into a full TTCN-3 test-execution toolchain in Go: a tree-walking
interpreter and an ntt exec runtime that can compile, execute and
verdict real TTCN-3 test suites, validated against the ETSI TTCN-3
conformance suite.

ETSI conformance: 4784 / 4948 matched (97.14%), tracked as a CI-style
regression gate.

Draft — this is a large branch (295 commits) opened for visibility and
review rather than immediate merge. Happy to split it into reviewable
chunks if that's preferred.

What's included

  • Interpreter (interpreter/) — tree-walking evaluation of modules,
    testcases, functions/altsteps, templates, alt/interleave, ports
    (message and procedure-based communication), timers, components, and
    TTCN-3 object orientation (classes, inheritance, nested classes).
  • Execution runtime (runtime/, runtime/exec/) — testcase
    executor, verdict handling, port/queue model, config (--cfg,
    [MODULE_PARAMETERS]), reporting, and a C ABI / cgo bridge so C/C++
    test ports can be driven from the Go runtime.
  • Codecs (runtime/codec/) — JSON and XML/XER encode/decode paths
    plus RAW encvalue/decvalue round-tripping.
  • Semantic analysis (ttcn3/semantic/) — additional static checks
    (attributes, parametrization, restrictions, type rules, …) surfaced
    through ntt check.
  • Conformance harness (conformance.go) — runs the ETSI suite,
    classifies each file's outcome against its @verdict annotation, and
    gates regressions against a committed baseline
    (testdata/conformance-baseline.json).

Testing

  • Full conformance run, gated at --regress 0.5 against the baseline,
    with a per-file diff requiring zero regressions for every change.
  • A product TTCN-3 suite is used as a runtime canary and stays green
    (17 pass / 0 fail / 2 expected inconc) across changes.
  • go test ./... for the touched packages.

Status & remaining work

The remaining ~140 conformance misses are documented in
docs/conformance/remaining-work.md:
large dedicated features (strict procedure-payload matching, async
multi-PTC message echo, structural type compatibility), a set of
contradictory/mislabeled suite fixtures that are intentionally left as-is,
and risky semantic rejections that would regress positive tests. The
miss inventory and per-slice history live alongside it under
docs/conformance/.

🤖 Generated with Claude Code

rafael2knokia and others added 30 commits June 1, 2026 16:24
The complement of the lazy/fuzzy-call rule: actual parameters
passed to out / inout formal parameters cannot be references to
lazy or fuzzy variables, because the variable's value is not
yet evaluated when the call occurs and the side-effect target
is ill-defined (ETSI ES 201 873-1 5.4.2).

We collect every ValueDecl with a Modif of @lazy / @fuzzy into
a name -> modifier map and, for every actual argument passed to
an out/inout formal, check whether the bare identifier is one of
them. New diagnostic "lazy-fuzzy-var-to-out-inout" reports the
violation.

Cohort impact:
- 050402_actual_parameters: -4 misses (125, 126, 127, 128).
Total: 72.63% -> 72.71% (+4 matched fixtures, 3549/4881).

Tests added: lazy var to inout reject, fuzzy var to out reject,
plain var to inout accept.
….4.2)

ETSI ES 201 873-1 clause 5.4.2 says "all parameterized entities
specified as an actual parameter shall have their own parameters
resolved in the top-level actual parameter list". A bare
reference to a template that declares formals (e.g.
`f_test(mw_rec)` where mw_rec takes a template integer) is
illegal - the user must write `mw_rec(<args>)`.

We collect every TemplateDecl with a non-empty FormalPars into a
name set, then for each actual argument check whether the bare
identifier matches a parameterized template name. The new
diagnostic "parameterized-template-without-args" reports the
offending callee + arg index.

CallExpr arguments (`mw_rec(omit)`) are not Idents so they fall
through silently.

Cohort impact:
- 050402_actual_parameters: -1 miss (114).
Total: 72.71% -> 72.73% (+1 matched fixture, 3550/4881).

Tests added: bare reference reject, wrapped call accept.
ETSI ES 201 873-1 clause 22.3.1 restriction h forbids both
`[else]` clauses and altstep invocations in the response /
exception-handling block that trails a procedure-based call
(`p.call(S:{...}) { ... }`).

call_stmt_rules.go walks every CallStmt, iterates its Body's
CommClauses, and emits:

  * call-block-else-clause      when CommClause.Else is set
  * call-block-altstep-invocation when CommClause.Comm is an
                                  ExprStmt whose CallExpr targets
                                  a known module altstep
                                  (collected by walking FuncDecls
                                  with KindTok == ALTSTEP).

Plain getreply / catch alternatives fall through untouched.

Cohort impact:
- 220301_call_operation: -2 misses (NegSyn_001 else, NegSyn_002
  altstep).
Total: 72.73% -> 72.77% (+2 matched fixtures, 3552/4881).

Tests added: else reject, altstep reject, clean getreply accept.
ETSI ES 201 873-1 22.2.2 / 22.2.3 / 22.3.2-22.3.6 / 22.4 share
the rule that an AddressRef appearing in a port-operation's
`from` clause must not hold the value `null` at the time of the
operation.

null_addr_rules.go covers the trivially static case:

  1. collectStaticallyNullVars walks the enclosing FuncDecl
     body, picks up every `var T x := null;` declaration, and
     drops the candidate whenever `x` later appears on the LHS
     of a plain `x := <expr>` assignment.
  2. We then walk every BinaryExpr whose Op is FROM (the parser
     models `<port-op> from Y` as such a node) and emit
     `from-clause-null-address` when Y resolves to one of those
     never-reassigned null variables.

Multicast lists, field-write reassignments and inout passing are
intentionally ignored - the rule fires only when the var is
provably still `null` at compile time, which matches the ETSI
NegSem fixtures and avoids over-flagging legitimate code.

Cohort impact:
- 220202_receive_operation: -1 (NegSem_015)
- 220203_trigger_operation: -1 (NegSem_015)
- 220302_getcall_operation: -1 (NegSem_009)
- 220304_getreply_operation: -1 (NegSem_006)
- 220306_catch_operation: -1 (NegSem_006)
- 2204_the_check_operation: -1 (NegSem_001)

Total: 72.77% -> 72.89% (+6 matched fixtures, 3558/4881).

Tests added: null-var rejected, reassigned var accepted, literal
`mtc` reference accepted.
Three small extensions to actual-parameter and port-redirect
analysis covering the next-largest cohorts of NegSem misses.

actual_param_rules.go now records each formal parameter's
declared type name so it can run two new checks:

1. port-parameter-non-port-arg: any non-port-typed actual passed
   to a port-typed formal (ETSI 5.4.2). Literals are rejected
   outright, idents are rejected when they resolve to a local
   var declared with a non-port type. Component-port refs and
   unknown idents fall through.

2. uninitialised-arg-to-in-inout: a local var declared without
   an initialiser AND never bound (no plain `x := y`, no
   `x.f := y`, no `x[i] := y`, no `-> value x` redirect) cannot
   be passed to a non-template `in` / `inout` formal (ETSI 5.4.2).
   The collector runs per-FuncDecl to avoid cross-scope shadowing
   where the same name is bound in one scope and unbound in
   another.

receive_redirect_rules.go: extend the existing
value-redirect-without-template check from `receive` to
`trigger` and `check` (ETSI 22.2.3 / 22.4) - they share the
same "no source type without a template arg" constraint.

Cohort impact:
- 050402_actual_parameters: -3 (NegSem_099 port, NegSem_119/120
  unbound).
- 220203_trigger_operation: -1 (NegSem_012 value-redirect).

Total: 72.89% -> 72.98% (+4 matched fixtures, 3562/4881).

Tests added: port-arg reject + accept, unbound-var reject,
partial-init accept, redirect-bound var accept.
ETSI ES 201 873-1 21.1.2 enforces strong typing on every
configuration-operation argument: if the component type is
known (from the calling function's runs-on / system clause,
from a typed var, or from self / mtc / system), the referenced
port instance must be declared in that component's flattened
port set (own ports + extends-chain).

connect_map_compat.go used to silently skip the diagnostic when
the port lookup failed. We now:

1. flattenComponentPorts: traverse the extends-chain (via
   collectComponentParents) so inherited ports count as
   present.
2. resolvePortRefStatus: distinguish three outcomes -
   portRefUnknown (variable not resolvable; stay quiet),
   portRefMissingPort (component known, port missing), and
   portRefOK.
3. checkConnectMapInBody: emit "port-ref-not-in-component" for
   each side that comes back portRefMissingPort, before
   short-circuiting the rest of the compatibility checks.

resolvePortRef is kept as a thin wrapper around the new status
helper to avoid touching the other callers.

Cohort impact:
- 210101_connect_and_map_operations: -5 (NegSem_008/010/011/013/014)
- 210102_disconnect_and_unmap_operations: -5 (NegSem_008/010/011/013/014)

Total: 72.98% -> 73.18% (+10 matched fixtures, 3572/4881).

Tests added: unknown port rejected, extended-component accepted,
inherited port accepted.
ETSI ES 201 873-1 21.3.2 (start) and 21.3.10 (call) forbid the
behaviour passed to a component operation from returning a value
of port, default or timer type.

component_ops.go: extend funcMeta with returnKind / returnName
captured from FuncDecl.Return.Type (promoting declared port-type
identifiers to a synthetic PORT kind, mirroring the existing
formal-param promotion). checkStartArgs now emits a new
"start-forbidden-return-kind" diagnostic whenever the resolved
return is PORT, TIMER, or the literal `default` type identifier.

The check fires for both `comp.start(f())` and `comp.call(f())`
because they share the checkStartArgs entry point.

Cohort impact:
- 210310_call_test_component_operation: -3 (NegSem_012 port,
  NegSem_013 default, NegSem_014 timer).

Total: 73.18% -> 73.24% (+3 matched fixtures, 3575/4881).

Tests added: port-return reject, timer-return reject, integer-
return accept.
ETSI ES 201 873-1 6.2.7 lets array declarations carry an
explicit index range (e.g. var integer v_arr[2..5]). The
existing array-index checker only handled the plain [N] form
and assumed bounds 0..N-1, so out-of-range accesses on
custom-bound arrays slipped through.

array_index_rules.go:
- arraySpec gains lo / hi / boundsKnown for the outermost dim.
- arraySpecFromDims replaces dimsTotalSize: each dim is parsed
  via dimRange, which recognises both `N` and `lo..hi` BinaryExpr
  shapes. The outermost dim's bounds are captured; inner dims
  still fold into the total slot count for the legacy fallback.
- checkArrayIndexExpr uses boundsKnown to check lo..hi inclusive
  when available; falls back to 0..size-1 otherwise.
- intLiteral64 (int64) is added alongside the existing
  length_constraint.go intLiteralValue (int) to avoid the
  redeclared-symbol clash.

Also dropped the early `len(arrayTypes) == 0` short-circuit in
checkArrayIndexRules - modules with only inline `var T x[N]`
decls and no module-level subtype now still get the check.

Cohort impact:
- 060207_arrays: -4 (NegSem_022/023/024/025 custom-range).
- Side effect: -1 in 220302_getcall_operation (the new
  null-addr rule covers fixture 007 once arrays are no longer
  the early return).

Total: 73.24% -> 73.35% (+5 matched fixtures, 3580/4881).

Tests added: custom lower / upper out-of-range reject and
in-range accept.
ETSI ES 201 873-1 6.3.1 forbids assigning a value outside the
declared range to a constrained subtype variable. The existing
checkValueInBody only inspected literal RHS expressions, so a
trivial level of indirection (`var integer v_int := 15; v_c :=
v_int;`) walked past it.

value_constraint.go: when the RHS of an assignment is a bare
Ident, look it up in collectLiteralInitVars (a body-local map
of name -> literal initialiser) and re-run numericLiteralValue
against the recorded literal. The collector drops any var that
ever appears on the LHS of a later assignment, so the resolver
never trusts stale literals.

Cohort impact:
- 060301_non-structured_types: -2 (NegSem_001/002 indirect
  literal range violation).

Total: 73.35% -> 73.39% (+2 matched fixtures, 3582/4881).

Tests added: bare-ident reject, in-range bare-ident accept,
reassigned-ident silently passes.
ETSI ES 201 873-1 6.2.3.2 caps the index of a length-constrained
record-of / set-of value at length(...) - 1. The existing array
checker recorded an arraySpec{size: 0} for every record-of / set-
of subtype, so out-of-range writes against `type record length
(0..N) of T Foo;` silently passed.

array_index_rules.go:
- collectArrayTypes: prefer the ListSpec.Length field over
  SubTypeDecl.Field.LengthConstraint (the parser attaches the
  length to the inner list spec in `type record length(...) of T
  Name;`). When the upper bound resolves to a positive integer we
  build arraySpec{lo:0, hi:N-1, boundsKnown:true, size:N} so
  checkArrayIndexExpr can flag out-of-range indices both lower
  and upper.
- lengthExprMax: shared helper that pulls the inclusive upper
  bound out of either a fixed `length(N)` form or a `length(lo..
  hi)` BinaryExpr. Unbounded / non-literal bounds fall through
  and leave the spec at size==0 for the legacy fallback.

Cohort impact:
- 060203_records_and_sets_of_single_types: -2 (NegSem_011 record-
  of LHS, NegSem_012 set-of LHS).

Total: 73.39% -> 73.43% (+2 matched fixtures, 3584/4881).

Tests added: record-of and set-of out-of-bound reject, within-
bound accept.
ETSI ES 201 873-1 5.4.2 NOTE: actual parameters passed to inout
formal value parameters must be variables, formal value
parameters or references to elements of variables of structured
types. Individual string elements (`charstring v[0]`) are
explicitly NOT references to elements of structured types and
are therefore disallowed.

actual_param_rules.go adds a `string-element-to-out-inout`
diagnostic that fires when:
- the formal parameter direction is `out` or `inout`, AND
- the actual is an IndexExpr whose base resolves (via
  collectLocalVarTypeNames) to one of the string family types
  charstring / universal charstring / bitstring / hexstring /
  octetstring.

stringElementBase is a small helper that returns the base var
name when the pattern matches. Anything else (plain variable,
composite field access, unknown identifier) falls through.

Cohort impact:
- 050402_actual_parameters: -1 (NegSem_097).

Total: 73.43% -> 73.45% (+1 matched fixture, 3585/4881).

Tests added: string-element rejected, plain var still accepted.
CONTRIBUTING.md tips section asks for `gofmt`-clean code. Only
doc-comment list-marker reflow and minor whitespace, no
behaviour change.
ETSI ES 203 022 5.1.1.5 imposes three lint-grade rules on class
declarations that we can catch with pure syntax:

1. constructor-out-inout-param: a `create(...)` constructor may
   only declare `in` formal parameters; `out` / `inout` are
   forbidden. We walk every ConstructorDecl directly.

2. class-field-self-init: the initialiser of a class field
   cannot reference the field being initialised. `var integer
   v_i := v_i + 1;` is cyclic.

3. class-field-uninit-ref: the initialiser of a class field
   cannot reference a sibling field that itself has no
   initialiser. The sibling is unbound when the implicit
   constructor runs.

checkClassFieldInitRefs walks every class declaration, snapshots
each member field's "has init" state via
collectClassFieldInitState, then re-walks the field initialisers
and emits a diagnostic per offending Ident reference. Only bare
Ident references to known sibling field names are diagnosed;
`this.x` selectors, function calls and complex expressions fall
through silently.

Cohort impact:
- 5010105_constructors: -4 (NegSem_001 out, NegSem_002 inout,
  NegSem_010 self-ref, NegSem_011 sibling-uninit-ref).

Total: 73.45% -> 73.53% (+4 matched fixtures, 3589/4881).

Tests added: out reject, in accept, self-init reject, uninit-
sibling reject, initialised sibling accept.
The cfg loader has parsed [MODULE_PARAMETERS] into a map[string]string
since the runtime/cfg package landed (cfg.File.ModuleParameters), but
no production caller actually used the map: exec.Run only asked for
ExecuteList, the Driver interface had no surface to push overrides
into the interpreter, and the interpreter only ever evaluated the
in-module default expressions. PX_TOKEN, PX_NODE_NAMES, PX_DAEMON_PORTS,
PX_FIXTURE_* all silently fell back to their in-source defaults; the
time-sync-monitor handoff (NTT_GAPS gap #7) flagged this as the root
cause of every non-pass verdict on that suite.

This change plumbs the override end-to-end with three coordinated
pieces:

1. runtime/exec: new ModuleParamSetter optional interface. exec.Run
   does a type-assertion on the driver before scheduling cases and,
   when the cfg carries a non-empty [MODULE_PARAMETERS] section,
   pushes the map via SetModuleParameters. A non-nil error aborts the
   suite (a bad override is a configuration bug, not a runtime fault).

2. interpreter: new RunTestcaseWith(trees, qname, opts) that accepts a
   TestcaseOptions struct carrying the override map and an optional
   warning callback. The existing RunTestcase signature is preserved
   as a thin wrapper so every existing caller keeps working. The
   override pass runs after the module-init phase (so in-source
   defaults are bound first) and before the testcase body fires
   (so the override wins). Unknown keys and unparseable values fire
   the warning callback but never abort the run, matching Titan's
   runtime-warning behaviour.

3. interpreter/module_params.go: literal parser for the cfg value text.
   v0 supports the four primitive forms every PX_* knob in the
   time-sync-monitor suite uses (quoted charstring, integer, float,
   true/false), tolerates the trailing `;` Titan-style cfgs sprinkle,
   and handles the two common string escapes. Record-of / template
   overrides will need the full parser path; for now they surface as
   an "unsupported value" warning so users see the gap instead of
   silently inheriting the default.

The cmd-line driver (exec.go staticDriver) implements SetModuleParameters
by snapshotting the map and threading it through RunTestcaseWith on
every case; warnings go to stderr as "module parameter: ..." lines.

Reproducer from NTT_GAPS gap #7:
  cat > /tmp/smoke.cfg <<EOF
  [MODULE_PARAMETERS]
  MonitorTestCases.PX_DAEMON_PORTS := "11111,22222,33333";
  [EXECUTE]
  MonitorTestCases.tc_PartialPods_V1
  EOF
  ntt-cabicgo exec --cfg /tmp/smoke.cfg .

Before: inconc "needs >=2 configured pods (got 1)" -- override dropped,
        lengthof(f_csvToIntList("50056")) == 1.
After:  fail "connect: Connection refused"        -- override applied,
        all three ports parsed; failure is now the unimplemented
        HttpServer_PT.cc (separate work, owned by time-sync-monitor).

Tests:
- 6 new interpreter tests cover charstring / integer / boolean
  overrides, trailing-`;` tolerance, unknown-key warnings, bare-name
  acceptance.
- 2 new exec tests cover that exec.Run actually invokes
  SetModuleParameters with the cfg map and that a setter error aborts
  the suite.

Conformance: unchanged at 73.53% (no ETSI fixture depends on
cfg-driven modulepar overrides; the bar moves on the time-sync-monitor
side as predicted: gap #7 closed, remaining 6 fails are now the
HttpServer_PT.cc work the monitor team owns).
Four narrow static checks lifted from the ETSI 22.x / 6.x cohorts:

1. port-op-wrong-port-kind (bare-selector form). The kind violation
   for `<port>.<op>` used as an alt-branch head or standalone
   statement is now caught alongside the existing `<port>.<op>(...)`
   form. The bare-selector shape parses as a SelectorExpr inside an
   ExprStmt, not a CallExpr, so the original walker missed it.
   checkBarePortOpKind handles only the kind violation (no args,
   no template/type to validate).

2. value-redirect-forbidden-op. ETSI 22.3.2 forbids `-> value v`
   redirection on getcall and raise - both operations have no
   yielded value to bind. The receive_redirect_rules walker now
   emits this diagnostic when the redirect's outer op is getcall
   or raise.

3. any-from-non-port-array / any-from-non-port-ref. ETSI 22.2.2
   restriction g: `any from <X>.<port-op>` / `all from <X>.<port-op>`
   require <X> to be a port-array variable identifier. We catch:
     - single-port instances (NegSem_220302_004 / _007 family) -
       the port exists but is declared without an array dim;
     - non-port-typed members (NegSem_220302_020) - the receiver
       is a `var anytype p;` / `var T p;` declared on the
       runs-on component but T is not a port type.
   collectComponentPortInfo carries the per-instance isArray flag;
   collectComponentMemberVarTypes pulls non-port member-vars off
   the component so the function body sees them.

4. element-value-constraint-violation. ETSI 6.2.7 element subtype
   ranges (`type integer A[5] (1..10)`) and `record of <numeric>
   (1..10)` element constraints are now validated against literal
   composite initialisers. valueSpec gained an elementBound flag;
   checkElementConstraint iterates the composite-literal elements
   when set.

Cohort impact:
- 220302_getcall_operation: -2 (NegSem_001 bare-selector kind,
  NegSem_004 single-port any-from, NegSem_020 non-port any-from).
- 060207_arrays: -1 (NegSem_001 element value-constraint).

Total: 73.53% -> 73.61% (3593 matched fixtures).

Tests: 6 new unit tests cover the four new diagnostics plus the
positive cases for port-arrays and properly-bound elements.
Three narrow static checks for the structured-types cohort:

1. array-size-mismatch. ETSI 6.3.1 array-size compatibility:
     type integer A[1];
     var integer v_int[2] := { 5, 4 };
     var A v_a;
     v_a := v_int;             // 2 -> 1: rejected
   collectSubtypeArraySizes lifts the dim off the subtype
   declaration; collectLocalArraySizes does the same for `var T
   x[N] := ...`; collectArrayLiteralInits captures the count of
   composite-literal initialisers. sourceArrayLen resolves the
   RHS to a literal count or a local-array dim. Direct
   initialiser flagged at the ValueDecl site; subsequent assigns
   flagged at the BinaryExpr.

   compositeLiteralLen explicitly skips literals that use
   indexed-/named-field assignment (`{[1] := 1}`, `{f := 1}`) -
   those don't pin a positional count and an array of size 5
   with `{[3] := 1}` is a valid partial initialiser.

2. mixed-literal-notation. ETSI 6.2 "assignments to fields or
   indexes given in list notation are not allowed":
     { 1, [0] := 3 }            -> [0] already covered, reject
     { 1, 2, [2] := 3 }         -> [2] outside positional run, ok
     { 1, 2, f1 := 3 }          -> f1 already covered (records), reject
     { 5, f3 := 3.14, f2 := "" } -> ok, positional only covers f1
   collectRecordFieldOrder grabs each `type record T { f1, f2,
   ... }` ordered field list; collectVarRecordType maps `var T
   x` declarations to that list; findEnclosingRecordFields ties
   a composite literal back to its receiver type so the named-
   field overlap can be computed.

   Indexed overlaps are flagged when every `[i]` is a literal
   integer AND the lowest such i is < the positional count.
   Named-field overlaps need both the literal and the record's
   field-name order.

3. length-constraint-violation via bare-ident RHS + CONCAT.
   collectStringLiteralInits and lengthOfRHS extend
   length_constraint.go's assignment-side check to resolve
   `v_cc := v_c` against a captured `var charstring v_c := "jk"`
   and to compute the length of `&` concatenations (NegSem
   060301_009-012 use both forms).

Cohort impact:
- 060301_non-structured_types: -3 (NegSem 009, 011, 012 - bare
  ident RHS).
- 060207_arrays + 060301: -3 (NegSem 007, 008 and other array
  size mismatches).
- 0602_toplevel: -5 (NegSem 005-009 mixed-literal overlaps).
- One pre-existing array-size mismatch (Sem_24) now reported as
  semantic-error correctly (no net change).

Total: 73.61% -> 73.84% (3605 matched fixtures, +12 over previous
batch).

Tests: 8 new unit tests cover the array-size cases (mismatch,
indexed-init bypass), the mixed-literal cases (overlap and
non-overlap, indexed and named), and the bare-ident length-
resolution path.
This is local CLI tooling state; it shouldn't have been tracked.
Untrack the symlink that slipped in via the previous commit and
add an explicit gitignore entry.
Two new rule modules covering the call-on-component (ETSI 21.3.10)
and uniqueness-of-identifiers (ETSI 5.2.2) cohorts.

1. component_call_rules.go (new). The `<X>.call(<funcCall>)`
   test-component operation is disambiguated from the
   port-procedure `<port>.call(Sig:tmpl)` shape by checking that
   the single actual is itself a CallExpr (no `Type:` qualifier).
   Three diagnostics:

   - call-on-non-component: receiver var is declared with a type
     that isn't a component (e.g. `timer t; t.call(f())`).
   - call-forbidden-param-type / call-forbidden-return-type: the
     callee's formal-parameter or return type contains a port,
     timer or default - either directly or nested through a
     record/set/union field. The walker reuses the existing
     fieldsOfStruct map from subset_superset.go and recurses with
     a visited set to block cycles.

   The builtin `timer` and `default` keywords are matched by
   literal name; user-defined port types come from
   collectPortTypes.

2. identifier_uniqueness.go (new). A per-block-scope walker
   pushed/popped on every BlockStmt, ForStmt, WhileStmt,
   DoWhileStmt, IfStmt branch, AltStmt, SelectStmt CaseClause
   and CommClause. Three diagnostics:

   - duplicate-identifier-in-scope: two var/const declarations
     in the same scope share a name, or a body decl shadows a
     formal parameter.
   - identifier-shadows-component-member: a body decl reuses a
     name from the function's runs-on component (member vars,
     ports, constants, timers; flattened across `extends`).
   - identifier-shadows-module-def: a body decl reuses a name
     from the enclosing module's top-level def list - or the
     module's own name.

   The scope stack respects per-loop scope so two sequential
   `for (var integer i := 0; ...)` blocks don't collide.

Cohort impact:
- 210310_call_test_component_operation: -5 (NegSem 004 non-comp
  receiver, NegSem 009-011 nested forbidden params, NegSem 015-016
  nested forbidden return).
- 050202_Uniqueness_of_identifiers: -9 (NegSem 001, 004-011).

Total: 73.84% -> 74.13% (3619 matched fixtures, +14 over the
previous batch).

Tests: 8 new unit tests cover both rules: component-call
non-component receiver, nested-forbidden-param, the four uniqueness
diagnostics (component-shadow, module-shadow, module-name-shadow,
duplicate-in-body, param-shadow), and the negative case where
sibling for-loops re-using `i` must not conflict.
Pull six NegSem_160102_* fixtures from PASS to REJECT:

- regexp(s, p) without a group index is now flagged as
  regexp-missing-group-index (the std requires 3 args).
- regexp(s, p, n) with n past the number of literal capturing
  groups in the pattern is regexp-group-index-out-of-range.
  Pattern groups are counted by scanning the literal for
  unescaped `(`; non-literal patterns silently fall through.
- rnd(infinity) / rnd(-infinity) / rnd(not_a_number) is
  rnd-non-finite-seed (Annex C.5.6.2).
- substr(t, ...) where t is a template carrying a non-
  AnyElement matching mechanism (`*` inside a bit/hex/oct/
  char literal, or bare `*` in a record-of composite literal)
  is substr-forbidden-matching-mechanism. Covers both the
  string-literal form ('00101*'B) and the list-literal form
  ({7, 8, *}).
- sizeof(t) where t is a template anytype is rejected with
  the existing sizeof-on-variable-shape code, reason
  "anytype" (sizeof is only defined for fixed-shape
  structures).

To make the substr rule reach `var template T x := ...`
declarations the template-name collector now also walks
ValueDecls whose KindTok is TEMPLATE or that carry a
TemplateRestriction, so `var template bitstring Mytemp`
appears under the same lookup as `template bitstring
Mytemp`.

Conformance: 74.13% -> 74.25% (+6 net, 0 regressions).
Closes the time-sync-monitor NTT_GAPS #8 blocker: the alt
scheduler's no-match path used to fall through to a verdict-
prefering legacy heuristic and fire a real-port-receive clause
body on an empty queue, leaving `-> value req` bound to
runtime.Undefined. With the FakeDaemon shape that produced a
JSON {"connectionId": null, ...} the C++ HttpServer_PT couldn't
route back to any live connection and every daemon-driven test
in the suite timed out.

Three coordinated changes:

1. runtime/testcase.go grows MessageReady (cap-1 chan) plus
   per-PTC PTCExit envelopes. EnqueueMessageFrom signals the
   ready chan on every enqueue so a parked alt scheduler can
   re-enter; Stop() and StopPTC(refID) signal it too so a
   `mtc.stop` or `d.stop` wakes the parked goroutine. Also
   moves the component "current" stack onto a per-goroutine
   sync.Map (keyed by GoroutineIDFn published from the
   interpreter's fast-goid init) so concurrent PTCs don't
   trample each other's sender tagging.

2. interpreter/testcase.go's evalAltStmtBestEffort, after the
   defaults pass and the existing defaultCtx guard, now parks
   on waitForAltPortTraffic when the alt's receive port has an
   external driver bound (altHasExternalPortGuard). The wait
   selects on MessageReady, the PTC's StopChan, and a 50 ms
   tick; a successful wake re-enters the alt's first pass via
   a tail call (MaxEvalDepth-bounded). Loopback-only alts
   (the conformance suite's default shape) keep the legacy
   verdict-prefering heuristic because no MessageReady signal
   will ever arrive for them.

3. interpreter/interpreter.go forks `comp.start(f)` into a
   goroutine when the receiver is alive AND the function body
   matches startBodyBlocksOnPortReceive (an alt whose every
   clause is a port-receive guard with no [else] / timer /
   plain-expression branch). RegisterPTC stashes the cancel
   envelope, the goroutine pushes/pops its component ref on
   its own goroutine-local stack, sets ref.Done=true on
   return, and signals FinishPTC. `comp.stop` / `comp.kill`
   on the ref close StopChan and signal MessageReady so the
   parked alt unwinds within a single 50 ms tick. The
   surrounding RunTestcaseWith calls WaitPTCs(5s) before
   reporting the verdict so a daemon left running after the
   MTC body finished still gets joined (or hard-stopped
   inside the 5 s budget).

Gates kept tight so the conformance suite is unaffected:

  - async fork only when ref.AliveModifier && body has a
    port-receive-only alt -> sequential .start;.done;.start
    fixtures still observe in-order side-effects.
  - alt wait only when the receive port has an external
    PortDriver bound -> loopback fixtures still fall through
    to the heuristic that the suite's procedure-based check /
    receive alts depend on.

Conformance: 74.25% (unchanged from master baseline; the only
diff in the verdict matrix is non-deterministic map-iteration
order in one Sem_B010506 error string).

New unit tests:

  - TestAsyncPTC_AltOnEmptyQueueDoesNotFireBody: the gap #8
    reproducer reduced to a self-contained TTCN-3 program; we
    assert the alt body never ran and the testcase joined in
    well under the 5 s WaitPTCs cap.
  - TestAsyncPTC_InjectWakesAltAndBodyRuns: the inverse; a
    sibling goroutine pushes a SrvRequest via the cabi
    inject-equivalent (CurrentExec().EnqueueMessageFrom), the
    parked alt wakes, the matching clause fires exactly once
    and the verdict lands pass.

Expected downstream effect for time-sync-monitor: 6 pass / 2
inconc / 11 fail -> 17 pass / 2 inconc / 0 fail (Titan parity).
Working memo for the V4 -> V5.1.1 changes that affect ntt's
semantic checks + the shape of the next conformance suite.

Highlights: procedure-based comm + fuzzy/lazy templates +
shift/rotate + most automatic-type are moving out of core into
ES 201 873-12 (extensions); message keyword becomes optional on
port types; lowercase 'b/'h/'o string-literal markers accepted;
static-vs-dynamic templates replace the fuzzy concept; only
import all stays in core.

Action items at the bottom are not blocking - they're a parking
lot for the V5-suite work once the conformance fixtures are
regenerated.

Contact at ETSI: Matthias Simon (Nokia delegate).
Three coordinated fixes that take the time-sync-monitor TTCN-3
suite from 6 pass / 11 fail / 2 inconc to 11 pass / 6 fail / 2
inconc (~3 s wall clock). The remaining failures are all socket
errors on the C++ test-port side (lingering accept connections /
TIME_WAIT), not the interpreter.

Gap #9 - PTC argument capture (snapshotPTCArgs)

  `d.start(f(arr[i]))` inside a `while (i < lengthof(arr))` loop
  used to fork the PTC goroutine and re-evaluate `arr[i]` lazily
  against the *parent*'s now-advanced `i`. The PTC ran with the
  zero-value entry instead of the per-iteration snapshot and the
  daemon bound to port 0.

  evalComponentMethod now resolves the function reference at
  .start time, eagerly evaluates each actual argument in the
  parent scope (including the RHS of `name := value` named args
  so applyFunctionWithCallSite can still re-map by name) and
  hands a snapshot []runtime.Object to the goroutine. Falls
  back to the legacy lazy eval(body) when the callee isn't a
  resolvable runtime.Function (e.g. inline lambda bodies).

Gap #10 - bare `T.timeout` / timer-only alt must block

  `timer T := 0.2; T.start; T.timeout;` previously returned
  immediately because we had no real clock; the same shape
  inside `alt { [] T.timeout {} }` short-circuited via the
  legacy verdict-preferring heuristic.

  TimerHandle now records StartedAt + DefaultDuration. Outside
  an alt, T.timeout (both the statement and the .timeout method
  call) waits on a time.Timer (gated by the testcase
  MessageReady channel so self.stop / mtc.stop unwinds the
  wait promptly). Inside an alt, T.timeout becomes a
  non-blocking expired/not-expired predicate; the alt scheduler
  itself (nextAltTimerDeadline + waitForAltTimerDeadline)
  sleeps for the soonest-firing timer when every clause is a
  bare T.timeout guard. Bare `T.start;` restores Duration to
  DefaultDuration per ETSI 23.2 - an earlier `.start(M)`
  override does not persist.

Gap #11 - drain port maps on `.stop` / `.kill` and on testcase teardown

  cabi/cgo C++ ports (e.g. the suite's HttpServer_PT) hold their
  TCP listener until on_unmap fires. Before this commit the
  bridge never called Unmap on PTC exit or `d.stop`, so the
  second testcase that re-used the same daemon port hit "socket
  error" before any traffic flowed.

  evalPortMap records every (compID, local, remote) triple in
  the TestcaseExec's compMapped table. Explicit `.stop` / `.kill`
  on a non-self ref now drains the target's mapped ports, and
  RunTestcaseWith's teardown invokes drainAllPortMaps after
  WaitPTCs has joined the PTC goroutines. We deliberately do
  NOT drain on natural PTC body exit - a daemon-style PTC
  that "completes" by returning needs its listen socket
  reachable while the MTC pulls responses (NTT_PORT_DEBUG=1
  lights up tracing for the drain paths).

Other touch-ups

  - runtime/object.go: TimerHandle grows StartedAt + DefaultDuration.
  - runtime/testcase.go: RecordPortMap / ForgetPortMap /
    DrainPortMaps / DrainAllPortMaps + PortMapEntry on
    TestcaseExec.

Tests

  - TestPTCArgSnapshot_LoopIndexNotCapturedByReference (gap #9):
    walks ports 50061/2/3 with a per-iteration .start, asserts
    each Bind carried the right tcpPort and the whole testcase
    finished in <4 s.
  - TestTimerBareTimeoutBlocks / TestTimerAltOnlyTimeoutBlocks
    (gap #10): assert a 0.2 s timer actually consumes ~200 ms
    of wall clock.
  - interpreter_test.go: drop the `a[-1]` case from TestIndexExpr;
    negative record-of indexing has been a runtime error since
    11ec7d8 and the assertion has been stale ever since.

Co-authored-by: Cursor <cursoragent@cursor.com>
A batch commit consolidating the static-check work mined across
the ETSI TTCN-3 conformance suite cohorts after gap #8 landed.
+182 net wins on the suite (3623 -> 3805 matched fixtures over
4948) without regressing any pre-existing Sem test.

New semantic rule files (ttcn3/semantic/):

  Component / configuration ops (cohorts 2103, 2101)
    component_array_init_rules.go    component arrays may not
                                     initialise from non-component
                                     literals (21.3.1)
    component_op_receiver.go         .start/.stop/.kill/.done/.running
                                     receiver must be a component
                                     handle, not a primitive
    component_test_op_rules.go       .done/.killed/.running rejected
                                     when receiver isn't a started
                                     PTC handle
    consecutive_start_rules.go       a second `.start` on the same
                                     non-alive component is rejected
    nonalive_restart_rules.go        `.start` after `.stop`/`.kill`
                                     on a non-alive PTC (21.3.2)
    start_arg_type_rules.go          start/call args may not contain
                                     port/timer/default types (21.3.4)
    connect_outlist_rules.go         keep helper machinery for the
                                     21.1.1 b3/c3 rule; emit no
                                     diagnostic until CR 7607 lands
                                     (Sem 210101_011/012 expect
                                     acceptance today)
    map_param_rules.go               `<op> param (...)` clauses
                                     validated against port-type
                                     declaration (21.1.2.5)

  Timer ops (cohort 23)
    any_timer_rules.go               `any timer.<op>` only allowed
                                     for .running / .timeout
    timer_arity_rules.go             T.start/T.stop arg-count checks
    timer_receiver_rules.go          timer ops only on timer-typed
                                     receivers; bare-ident statement
                                     forms rejected
    timer_scope_rules.go             timers may only be declared
                                     inside testcase / function /
                                     altstep bodies (23.2)
    timer_syntax_rules.go            `T.start()` with empty parens
                                     and stray-literal statements

  Communication ops (cohorts 2202, 2203, 2204)
    activate_rules.go                activate() arg must be a known
                                     altstep call (16.1.2)
    addr_clause_type_rules.go        from/to/sender clause types
                                     vs component / address / port's
                                     explicit `address <T>`
    call_operation_rules.go          port.call args + getreply/catch
                                     alt signature alignment (22.3.1)
    decoded_redirect_rules.go        @decoded target must be of one
                                     of the bit/hex/oct/char/universal
                                     charstring family (22.2.2 g)
    getcall_redirect_rules.go        getcall param redirect names
                                     must match the signature
    port_op_receiver.go              port ops only valid on port-typed
                                     receivers; reject bare-ident
                                     statement forms
    port_type_rules.go               port-type direction list + comm
                                     keyword sanity (6.2.9 / 6.2.10)
    raise_operation_rules.go         raise() target signature must
                                     declare matching exceptions
                                     (22.3.5)
    send_template_type_rules.go      send() arg must be a data type,
                                     not component / port / timer /
                                     default (22.2.1 h)
    signature_template_rules.go      template-signature literal
                                     direction matches signature

  Types / values (cohorts 06xx, 11, 15)
    composite_literal_rules.go       record / record-of literal
                                     shape vs type (6.2.1 / 6.2.3)
    enum_rules.go                    enumerated value list integrity
                                     (6.2.4)
    function_spec_rules.go           function/altstep/testcase
                                     formal-param-direction sanity
    index_out_of_bounds_rules.go     literal record-of indexing past
                                     declared bounds (6.2.3.2)
    length_bound_rules.go            length-constrained record-of /
                                     set-of bounds policed at the
                                     literal site (6.2.3.4)
    modified_template_self_ref_rules.go
                                     `template T modifies T` and
                                     parameter name / type mismatches
                                     against the base (15.5)
    modulepar_rules.go               modulepar default-value kind
                                     (16.2)
    open_type_rules.go               classification + restrictions
                                     on open / anytype
    recursive_type_rules.go          structurally recursive
                                     record / set without `omit`
                                     (6.2.1.4)
    select_stmt_rules.go             select-case branch type must
                                     match the discriminant
    template_reassign_rules.go       reject `template T x := y;
                                     x := z;` after the first
                                     binding (15.4)
    value_var_init_rules.go          non-template var initialisers
                                     may not use matcher constructs
                                     (?, *, range, pattern, decmatch,
                                     ifpresent, permutation /
                                     complement / subset / superset)
                                     - 11.1.d

Extended existing rule files:
  - actual_param_rules.go: deterministicArgViolation enforces
    16.1.4 (no side-effecting ops in fns passed to
    @fuzzy @deterministic / @lazy @deterministic formals);
    nonPortArgReason now allows the `skip` literal (`-`) for
    out port formals (5.4.1.2).
  - receive_redirect_rules.go: @index target type-check
    (must be integer-compat, arrays surfaced as `T[]`);
    value redirect type compatibility against the receive
    template (22.2.2 / 22.2.3); unwraps `any from`/`all from`
    FromExpr wrappers so the AST walks reach the inner CallExpr.
  - template_restrictions.go: param default-value vs enclosing
    restriction (15.8) - flags ?/* in template(omit/value)
    formals plus the `ifpresent` widener.
  - parametrization.go: port/timer-param direction rules
    disabled (V4.4.1+ relaxation; Sem_05040101_026/027/030/031
    ship as positive tests).
  - interleave_restrictions.go, control_part_ops.go,
    null_addr_rules.go, value_template_kinds.go,
    connect_map_compat.go, attributes.go: refinements
    that ride along with the new rules.

Parser plumbing
  - syntax/nodes.go: FormalPar gains Modif2 to carry the second
    of a stacked modifier pair like `@fuzzy @deterministic`.
  - syntax/parser.go: parseFormalPar now consumes up to two
    modifiers into Modif / Modif2 and silently absorbs any
    further @-prefixed tokens. ETSI ed. 4.13+ allows several
    modifiers per formal.
  - attr/attr.go: small cleanup so the modifier names line up
    with the new Modif2 surface.

Notable suppression
  - template_restrictions.go: modifiedTemplateRestrictionViolations
    is kept but no longer called from checkTemplateRestrictions;
    the strict 15.8 d transition rule contradicts
    Sem_1508_TemplateRestrictions_016..030 which document an
    explicit relaxation (unrestricted -> present, value ->
    present, ...).
  - connect_outlist_rules.go: similarly retains the helpers
    behind a no-op entry point pending CR 7607.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two `d.start(...)` calls in immediate succession used to race for the
cabi/cgo bridge's on_map dispatch: the second PTC's goroutine could
reach its `map(self:p, system:p)` body line before the first PTC did,
so the C++ port's pending_listens FIFO ended up out of start-order.
A later `ds[i].stop` -> drainComponentPortMaps -> on_unmap then
popped the wrong front and closed the wrong listener, breaking
`tc_DaemonStopMidTest_*` (which stops ds[0] mid-test and expects only
the matching daemon to go away).

Also wires drainComponentPortMaps into the bare `comp.stop` statement
form (ExprStmt -> SelectorExpr, no CallExpr wrapper); previously only
the parenthesised `comp.stop()` CallExpr branch drained.

`PTCExit` grows a `MapChan` (closed on the first successful Map call
via `SignalMap`, or as a fallback on FinishPTC so a body that never
maps doesn't dangle the parent). `comp.start` parks on MapChan with
a 50 ms ceiling before returning to the parent's next statement.

Time-sync-monitor suite (NTT_BIN=ntt-cabicgo, workarounds reverted):
6/11/2 -> 12/5/2. Remaining 5 failures are kernel TIME_WAIT on
port rebinds, addressable in the suite's C++ HttpServer_PT (SO_LINGER
0 / SO_REUSEPORT) not in ntt. ETSI conformance unchanged (77.93%).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ance)

Five narrow static checks aimed at the cohorts the previous batch
left on the table:

- charstring_range_rules.go (new): enforces ETSI 6.1.1 / 6.1.2.3 by
  walking `var <T> x := "..."` and `x := "..."` against any
  range subtype `subtype T charstring length(...) ("a" .. "z")`
  declared on T. Handles `char()` calls and both inclusive and `!`
  exclusive bounds; covers `charstring` and `universal charstring`.
  +6 NegSem_06010203_Ranges_007..015 (and one mixing fixture).

- inout_strict_typing_rules.go (new): enforces ETSI 5.4.2 by
  requiring the actual argument's declared type identifier to match
  the formal's verbatim when the formal is `inout`. Resolves the
  actual through Ident / SelectorExpr / IndexExpr (the last via a
  new collectListElementTypes helper that maps `record of T` /
  `set of T` to T). +3 NegSems in 0504_parametrization.

- raise_operation_rules.go: now checks the exception value's
  type against the signature's `exception(...)` list (ETSI 22.3.5).
  Uses literalTypeName / collectSignatureExceptionTypes to keep
  the inference deterministic; emits
  raise-exception-type-not-in-list on mismatch. +2 NegSems.

- send_template_type_rules.go: extended to receive / check /
  trigger per ETSI 22.2.2 o and 22.2.3 l. The component / port /
  timer / default reject path is shared via msgOpClauses to keep
  the diagnostic text uniform. +2 NegSems.

- value_var_init_rules.go: the existing init-time check now also
  fires on `x := <matcher>` assignments via a new BinaryExpr arm
  (ETSI 11.1 d). Adds value-var-template-assign. Net +1 NegSem_1901.

Net effect vs ntt-titan HEAD (66a0070): 3804 -> 3822 matched
(+18 wins, 0 regressions) on the full conformance suite. Pass rate
moves from 77.95% -> 77.97%; remaining headroom for the 80% target
lives mostly in 06_types_and_values (86 fails) and 2203 procedure
based communication (81 fails).

Co-authored-by: Cursor <cursoragent@cursor.com>
ETSI ES 201 873-1 clause 6.2.9:
  - Restriction d: formal parameters of `map param (...)` and
    `unmap param (...)` clauses shall be value parameters of a
    DATA type.
  - Restriction e: the in/out/inout type list of a message port
    shall reference DATA types only.

The new checkPortDeclTypeRules walks every PortTypeDecl. For each
PortMapAttribute it inspects every FormalPar; for each
PortAttribute on a message port it inspects every type ident in
the in/out/inout list. A new collectTypeAliasKinds helper
resolves one level of aliasing (`type default X;` -> "default",
`type port P X;` -> "port", etc.) so the rule fires through
trivial subtype chains, matching the conformance fixtures.

Emits `port-message-non-data-type` and
`port-mapparam-non-data-type` diagnostics. Conformance:
3822 -> 3828 matched (+6 net; +9 in NegSyn_060209 minus 2-3
flake from Sem_210301 tests that race the 5s exec budget against
their own 5s `timer t := 5.0; t.timeout` body).

Co-authored-by: Cursor <cursoragent@cursor.com>
Two narrow tightenings on the 06_types_and_values cohort:

- enum_rules.go now flags `Tuesday()` (empty user-value list) and
  `Tuesday(c_int, 5)` (mixed expression/literal in a multi-element
  list) per ETSI 6.2.4 v4.13.1. Single-arg expressions like
  `Tuesday(c_int)` and `Tuesday(2 + 3)` stay legal: the spec's
  STF-572 revision explicitly allows "an integer expression" in
  the single-element form. The earlier NegSyn_060204_001 / _002
  fixtures encode the pre-revision wording so they stay listed as
  expected-fail; only NegSyn_060204_004 (empty list) is the win.

- omit_value_rules.go now also walks module-level ValueDecl /
  TemplateDecl entries, not just FuncDecl bodies. A
  `const MyRecord c_rec := { ..., field3 := omit };` declared at
  the top of a module previously slipped through the
  optional-field check because the walker bailed at the function
  boundary. Covers NegSyn_060201_RecordTypeValues_001/_002 and
  NegSyn_060202_SetTypeValues_001/_002.

Net: 3828 -> 3834 matched (+6); 0 regressions vs the previous
HEAD baseline. Combined with 90c3589 the cumulative gain since
2316ffe is +12.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extends checkCharRangeSubtypeRules to validate
`var Constrained y; var charstring x := \"j5l\"; y := x;` by
remembering the literal each plain `var T x := <literal>` is
initialised with. The first re-assignment or shadowing decl
invalidates the entry to keep the inference conservative.

+2 NegSem_060301_non_structured_types_003 / _004 (charstring
range + universal charstring range via char(...)). Bitstring,
hexstring and length-constraint variants in the same cohort
need their own subtype-list / length rules; left for a separate
commit.

Conformance: 3834 -> 3836 matched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two adjustments to the modulepar kind/default-value checker:

- The walker previously only descended into ModuleParameterGroup
  nodes (the curly-braced multi-decl form). A bare
  `modulepar default X := null;` parses straight into a
  ValueDecl with KindTok=modulepar and was silently accepted.
  Now both shapes get the kind / matcher checks.

- Dropped the blanket "modulepar template T x" rejection - ETSI
  8.2.1 explicitly allows template-typed module parameters
  ("Module parameters are values or templates that may be
  supplied by the test environment at runtime"). The older
  NegSyn_0504_001 fixture (STF 409 v0.0.1) is from before the
  spec change and now flips to expected-fail.

- Added modulepar-value-matcher-default: a non-template
  modulepar's default expression must resolve to a value
  (no `?`, `*`, complement, ifpresent, value list).

Net conformance: 3836 -> 3837 matched (+2 wins on NegSem_080201
003/010, -1 on NegSyn_0504_001 which encodes the removed
template-modulepar restriction); 78.04% -> 78.08%.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds checkInterleaveBodyRules: every CommClause inside
`interleave { ... }` must use `[]`. A boolean guard like
`[v>0] p.receive(...)` and the `[else] { ... }` clause are both
rejected per ETSI ES 201 873-1 clause 20.4 restriction b.

Note: the broader 20.4 restrictions (no nested alt/interleave,
no break/continue/return/for/while/do-while/goto in a branch
body) were prototyped but conflict with the suite's own
Sem_2004_002..012 tests, which encode the revised
"for/while/do-while are allowed when the loop body has no
receive" exception (Sem_2004_004 even spells that exception out
in its header). We therefore narrow the static check to the
guard / else shape the suite agrees is illegal.

+2 NegSyn_2004_InterleaveStatement_001 / _002 wins, 0
regressions. Conformance: 3836 -> 3840 matched (the extra +2 is
the Sem_210301 timer-race flake reversing on this run).

Co-authored-by: Cursor <cursoragent@cursor.com>
rafael2knokia and others added 21 commits June 12, 2026 19:16
decvalue/decvalue_o/decvalue_unichar now decode hand-written JSON
payloads when the call names "JSON" explicitly: scalar coercion to
the declared output type, top-level Module.Type unwrapping, and
errorbehavior(ET_*:EB_IGNORE) semantics for undefined, enumerated
and constraint decode errors (Annex B.3.13).

The parser gains the OBJECT IDENTIFIER value notation
`objid { itu_t question(1) 7 }` as a first-class ObjidLiteral node;
the interpreter evaluates it to its arc-number list so loopback
send/receive matching round-trips.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
+6 matched: Pos_B313_error_behaviour_001/002/005/006,
Pos_B311_no_type_021, Pos_0702011_object_identifiers_001.

Add docs/conformance/refresh_artifacts.py so the baseline, miss
inventory and history refresh from one `ntt conformance --json`
report instead of ad-hoc edits.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The composite literal parser tolerates a trailing comma before the
closing brace (Titan accepts `{ x := 1, }` and ETSI positive
fixtures use it).

matchFile and the XSD loopback-transform recorder fall back to the
directory's sole same-extension file when a fixture names a sibling
test's reference file (ETSI copy-paste slips like Pos_..._008.ttcn
asking for Pos_..._026.xml).

XSD elements of type "anyType" now record an embed_values transform
applying B.3.10 restriction d: trailing empty strings are stripped
from embed_values on the typed receive. Modified templates
(`modifies`) record their declared type so receive-side transforms
key correctly.

Conformance: 4741 -> 4743 matched (96.28% -> 96.32%).

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AllRef kind selectors (`encode (const all except {...}) "Rule"`)
now attach attributes to the selected const/modulepar/template
definitions, and `<name>.encode` retrieval answers them with the
empty-list fallback of ETSI 27.8.

The semantic analyzer gains three rules: `except` references must
name definitions inside the with statement's scope (27.2), a plain
variant is rejected when several encodings apply (27.5), and a
setencode target must be listed in a port definition (27.9 a).

Subtypes apply the 27.1.2.2 multiple-encoding overwriting rules
against their underlying type: without an own encode the codec list
and variants are inherited per codec; with an own encode only
re-referenced codecs keep their variants. Multi-codec variant
values (`{"C1","C2"}."Rule"`) expand to one entry per codec,
import-with attributes layer onto explicitly imported definitions
(27.1.3), and `<value>.variant("Codec")` errors when the declared
type holds no such encode attribute.

Conformance: 4743 -> 4757 matched (96.32% -> 96.61%).

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The string rows still encoded the original upstream behaviour where
`?` / `*` acted as wildcards inside PLAIN charstring templates. The
matcher has since moved to the spec semantics - a plain charstring
compares literally and wildcards only act inside `pattern "..."`
templates (ETSI 15.11 / B.1.5, pinned by conformance fixture
Sem_1511_*_010) - so the wildcard expectations now build pattern
strings and the literal expectations assert literal compares.

Fixes the long-failing `go test ./builtins -run TestMatch`; the full
suite is green again.

%INT_NO_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
checkStructuredDeclRules rejects duplicate members in record/set/
union types and duplicate enumerated identifiers (ETSI 6.2.1, 6.2.2,
6.2.4, 6.2.5). The explicit enumerated value is deliberately left
unrestricted: the modern suite (Sem_060204_008, Syn_060204_003/004)
treats constant references and integer expressions as valid value
notations, superseding the older NegSyn_060204 fixtures.

Unquote now falls back to treating backslashes literally when a
literal is not a valid Go-escaped string - TTCN-3 USI quadruples
(\q{...}), pattern references (\N{...}), or a backslash before
whitespace - instead of erroring out or stripping the run as a line
continuation. Plain (universal) charstring values carrying such
content are parsed per ETSI 6.1.1, while the common C-style escapes
(\n, \t, \uXXXX) still resolve as before.

Conformance: 4757 -> 4761 matched (96.61% -> 96.67%).

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
decvalue / decvalue_o into an integer-typed output slot now decodes a
hand-written octetstring least-significant-octet first when the
round-trip cache misses - the symmetric counterpart of the
little-endian octetstring encvalue_o already produces for an integer
(`encvalue_o(10)` -> '0A000000'O). The cache path is consulted first,
so encvalue/decvalue round-trips are unaffected; this only adds a
decode for literal octetstrings that previously returned the
unspecified-failure code 1.

Conformance: 4761 -> 4762 matched (96.67% -> 96.69%).

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The var/const/template declarations inside an alt body are part of
the alt's evaluation, so a `repeat` re-runs them. They were evaluated
once before the repeat loop, which let an alt-local variable carry a
clause's mutation into the next round; ETSI 20.2 requires it to reset
each iteration. Evaluate them at the top of every round instead - the
first entry is covered too, so non-repeat alts are unaffected.

Conformance: 4762 -> 4763 matched (96.69% -> 96.71%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Template declarations now apply the same type-directed struct/union
coercion that value declarations already do, so a positional template
literal (`template T t := {1, true}`) maps onto its declared field
names. Without this, `t.field` access and `ispresent(t.field)`
resolved against a positional list and reported optional fields as
absent.

`omit(template)` (the omit restriction operation, ETSI 15.12) is
evaluated as identity on its operand - a complete value or `omit`.
The keyword form parses as a ValueLiteral, so it is intercepted
ahead of the builtin-name dispatch. Restriction violations
(`omit(?)`) remain a negative-test concern and still reach their
explicit setverdict(fail).

Conformance: 4763 -> 4764 matched (96.71% -> 96.73%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
evalPortReceiveInfo short-circuited the `from` filter for procedure
operations (`fromOk := isProc || fromAddrMatches(...)`), so
`p.getreply(S:?) from v_ptc` / `p.catch(...) from v_ptc` matched any
sender. The signature-template leniency (verdicts key off "a reply
arrived") is independent of the `from` address filter, which ETSI
22.3 honours for procedure ops too, so always evaluate it against the
envelope Sender.

Conformance: 4764 -> 4768 matched (96.73% -> 96.81%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
coerceToStruct only coerced positional outer lists, so a record/
template written with named-outer + positional-inner notation
(`{ field1 := {?, *} }`) kept its inner field positional and
`rec.field.subfield` resolved to Undefined. It now recurses into a
named record's declared fields and resolves inline anonymous struct
field types (new fieldStructTypeDesc handles both named refs and
`record { ... }` specs).

Exposing those inner fields surfaced an `ispresent` gap: a field set
to `*` (AnyOrNone) matches both a value and absence, so it is not
definitely present (ETSI 16.1.2) and ispresent now returns false for
it, while `?` (AnyValue) stays present.

Conformance: 4768 -> 4770 matched (96.81% -> 96.85%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `@verdict pass accept, noexecution` fixture is a parse/semantic
acceptance test that must not be executed. The harness already
honoured it for files with no testcase, but ran the first testcase
when one existed - so Syn_24_toplevel_002, whose first testcase walks
a setverdict(none/pass/inconc/fail) sequence ending in fail, was
scored fail. Report pass on clean parse+analysis for any
pass-expected noexecution fixture instead.

Conformance: 4770 -> 4771 matched (96.85% -> 96.87%).

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A class declared inside another (ETSI 5.1.1.10) now works: `Parent.Child`
resolves as a type reference, `v_parent.Child.create()` and a bare
`Child.create()` inside an enclosing method build the inner object, and
the inner methods read the enclosing object's members by their bare
names. nestedClassDesc locates the inner ClassTypeDecl and, when an
enclosing instance is supplied, binds a snapshot of its fields into the
inner closure scope; newMethodEnv exposes nested class names bound to
the running instance so a method can construct them with `this` as the
enclosing object.

Conformance: 4771 -> 4775 matched (96.87% -> 96.95%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`X ifpresent` evaluated to a blanket Undefined wildcard, so a present
value that did NOT match the inner template still matched (e.g. a
present 3 matched `(0..2) ifpresent`). It now produces a
runtime.IfPresent wrapper: matching accepts an absent field, or a
present value that matches the inner template X (ETSI B.1.4.2), and a
present non-matching value correctly fails. `ispresent` on an
ifpresent-template field is false - it admits absence, like AnyOrNone.

Conformance: 4775 -> 4776 matched (96.95% -> 96.97%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Values and templates of a `set of` type were built as ordered
(record-of) lists, so set-of matching compared positionally and
`{2,1}` failed to match `{1,2}`. A set-of subtype now records
TypeDesc.ListKind=SET_OF, and list values of such a type are tagged
unordered at declaration/coercion time - both directly
(coerceToDeclaredStruct) and for record/set fields whose type is a
set-of (coerceToStruct / coerceRecordFields recursion) - so the
existing order-independent matchSetOf path applies.

Conformance: 4776 -> 4777 matched (96.97% -> 96.99%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
decvalue(bitstring, intSlot) where the integer type declares a
variant "N bit" field width now decodes the most-significant N bits
into the slot, consumes them, and leaves any excess bits in the
caller's encoded slot. With fewer than N bits available it returns
the "not enough bits" code 2 and leaves both slots untouched, so the
output stays unbound (ETSI 16.1.2 / Annex C). Extends the existing
octetstring decvalue_o RAW path; the round-trip cache is still
consulted first, so encvalue/decvalue pairs are unaffected.

Conformance: 4777 -> 4780 matched (96.99% -> 97.06%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A record/set type with `with { optional "implicit omit" }` (ETSI
27.7) defaults every unspecified optional field to omit. An
uninitialised variable of such a type now pre-fills its optional
fields with omit at declaration (mandatory fields stay unset), so it
reads `{ omit, ... }` and compares equal to a value notation that
spells those fields out. Scoped to implicit-omit struct types, so
ordinary records are unaffected.

Conformance: 4780 -> 4781 matched (97.06% -> 97.08%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`connect(self:p, self:p)` is a loopback to self, so a component's own
sent message must be delivered back to itself. The self-sent-message
filter ignored it whenever the port was connected, which is only
correct for a connection to a different endpoint (the message went to
that peer). New TestcaseExec.ConnectedToOther distinguishes a
self-loop from a peer connection; self-sent messages are ignored only
for the latter. MTC<->PTC connections are unchanged.

Conformance: 4781 -> 4783 matched (97.08% -> 97.12%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RegexpExpr was a stub that returned Undefined. regexp(instr, pattern,
groupno) now matches the TTCN-3 pattern against instr and returns the
substring captured by the groupno-th parenthesised group (0-based;
group 0 is the first `(...)`), or the empty string on no match (ETSI
16.1.2 / Annex C.33). The new builtins.RegexpMatch reuses the existing
ttcnPatternToRegex translation, and @nocase is honoured.

Conformance: 4783 -> 4784 matched (97.12% -> 97.14%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture what is left after the incremental clean-win phase (97.14%,
141 real misses): the three buckets the remaining misses fall into -
large dedicated features (strict procedure matching, async multi-PTC
echo, structural type compatibility, ...), contradictory/mislabeled
fixtures that are intentionally not rejected, and risky semantic
rejections that would regress positives - with fixtures, what each
needs, effort/risk, and a suggested order.

%INT_NO_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI (`go test -race ./...`) failed on all platforms:

- runtime/env.go: Env had no concurrency control, but the interpreter
  runs parallel test components (PTCs) as goroutines sharing an
  enclosing scope chain. One PTC's Set raced another's Get walk over
  the same store map, which Go throws as "concurrent map read and map
  write". Guard the store with an RWMutex, released before recursing
  into `outer` so the chain never holds two locks at once.

- runtime/dap: the `launch` command emits events from a background
  goroutine, so TestServe_LaunchEmitsTerminated polled the output
  buffer while the server wrote it. Capture output through a
  concurrency-safe buffer in the test.

- internal/fs: TestJoinPath asserted a "//" base joins to "/c", whose
  cleaned form is OS-specific (Unix "/c" vs Windows UNC) - that is
  filepath.Clean's behaviour, not JoinPath's path-vs-URL routing, so
  drop the case.

Conformance unchanged (4784/4948, 97.14%); time-sync canary 17/0/2;
`go test -race ./...` green.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rafael2knokia rafael2knokia requested a review from 5nord June 16, 2026 10:41
Assigning a field of a previously omitted/uninitialised record field
(`v.sub.field2 := "abc"`) materialised a fresh record with only that
field. Under `optional "implicit omit"` (ETSI 27.7) the other optional
fields must default to omit, so the result is `{ omit, "abc" }`. The
field-assign path now resolves the lvalue's declared struct type and
implicit-omit inheritance (from the root variable) and pre-fills the
optional fields, mirroring the declaration-time behaviour. Scoped to
implicit-omit struct types, so ordinary records are unaffected.

Also refresh docs/conformance/remaining-work.md (drop this item).

Conformance: 4784 -> 4785 matched (97.14% -> 97.16%).
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rafael2knokia rafael2knokia marked this pull request as ready for review June 16, 2026 10:46
rafael2knokia and others added 6 commits June 17, 2026 11:13
Relabel a record/set returned through an out/inout parameter to the
caller lvalue's structurally-compatible type when the two have the same
field count but different field names (ETSI 6.3.2). coerceWritebackStruct
remaps the value's fields positionally on writeback, so for
`function f(out R1 p); f(v_r2)` the caller's v_r2 ends up with R2's field
names and compares / accesses correctly. It is a no-op on the common
same-type writeback path (field names identical), so blast radius is
limited to the structurally-compatible-but-differently-named case.

Fixes Sem_050402_actual_parameters_184.

Conformance: 4785 -> 4786 matched (97.16% -> 97.18%), 0 per-file regressions.
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the record/set field-name remap (ETSI 6.3.2) on the plain
`v2 := v1` assignment path, sharing remapStructByPosition with the
out/inout-parameter writeback. When the assigned value is struct-shaped
and both the source and target resolve to distinct struct declarations
of the same field count with different names, the value is relabelled to
the target type's field names so `v_r2.a` resolves correctly. A
struct-shape guard keeps scalar / string assignments off the
type-resolution path, and the remap is a no-op when field names already
match, so the common same-type assignment path is untouched.

Fixes Sem_060302_structured_types_001.

Conformance: 4786 -> 4787 matched (97.18% -> 97.20%), 0 per-file regressions.
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record the declared lower index bound of a constrained array subtype
(`type integer T[1..2]`) on TypeDesc.IndexOffset during subtype
registration, and have a list value assigned to a variable of such a
type adopt that offset so `v[lo]` reads the first element (ETSI 6.2.7 /
6.3.1). The list is copied before relabelling so the source array keeps
its own offset. arrayLowerBound is refactored to share arrayDefLowerBound
with the new registry path. The struct-shape guard keeps scalar / string
assignments off the type-resolution path entirely.

Fixes Sem_060301_non_structured_types_002.

Conformance: 4787 -> 4788 matched (97.20% -> 97.22%), 0 per-file regressions.
time-sync canary: 17 pass / 0 fail / 2 expected inconc.

%INT_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1d yields no clean win; capture findings so the items are not
re-attempted:

- External functions (160103_001/002): gaming — they assert host-defined
  returns keyed to the test name (return 1; input+1), not derivable from
  the signature. Also blocks 060302_010.
- encvalue_o RAW (160102_107/110): implementation-specific codec bytes;
  the loopback model has no real RAW codec.
- Template field-build / union-alt (150605_002): the lone legitimate
  feature, but a wildcard member-access propagation fix regressed 6
  (ischosen / record-set field referencing) for +1 because the type is
  gone at left==Any; reverted. Needs static type context, not a blanket
  rule.
- Control-part selection (2602_001): high-risk harness change.

Also refresh the suggested order: 1c essentially done, 1d exhausted, 1a
regresses; 1b / bucket-3 are the realistic next targets.

No code change.

%INT_NO_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eck)

Execution triage of the planned analyzer-rejection push (106 of 160
misses are reject->pass). No safe, simple static check was found:

- 0503_Ordering_002/003: contradictory (NegSem == positive Sem_0503_003/004;
  V4 must-precede vs V5 relaxed). Wash.
- 0901_communication_ports: full ETSI Figure 6/7 connection matrix, not
  "port twice"; positives connect one port to many peers. Only the
  two-TSI-port subset is a candidate (~2 tests, needs mtc/system detection).
- 220201/02/03 send/receive/trigger NegSem: runtime errors (disconnected
  port, @decoded decode failure, one-to-many missing `to`), not static.
- 150605 union-alt: irreconcilable without tracking the chosen union
  alternative (ischosen vs ispresent conflict); two attempts reverted.

Conclusion recorded: the safe-win seam is exhausted; remaining progress
needs a dedicated deep feature (multi-PTC scheduling, procedure-signature
qualification, union-alternative tracking, real RAW codec, or
connection-topology analysis).

No code change.

%INT_NO_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ore)

Attempted Slice 6 (Sem_060210 server-PTC echo). Forking the while(true)
receive-echo body as a goroutine (relaxing the AliveModifier gate on the
FakeDaemon fork path) makes it run, but the round-trip still fails: the
alt scheduler only parks-and-wakes (waitForAltPortTraffic) for ports with
an external driver (altHasExternalPortGuard); loopback alts use the legacy
verdict heuristic and never block for the peer's echo. Reverted.

Recorded that 1b's 6 tests are not one mechanism, and the two viable
paths (concurrent loopback alt scheduling, or a synchronous
message-responder analogous to the procedure RunDeferredResponders) are
both substantial and not clean commits.

No code change.

%INT_NO_SW_CHANGE
%AI=CLAUDE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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