Table of Contents

Namespace Hi.NcParsers.ParsingSyntaxs.Siemens

Classes

SiemensCallStatementSyntax

Captures a whole-line Siemens subprogram-call statement into Parsing.SiemensCall for SiemensSubProgramCallSyntax to resolve (inline the file) or safe-skip (structured warning, zero motion). Two shapes are owned:

  • Parenthesized call, any identifier — HQ_FC(1,50,75,160), L_39(0.000,0.000), HH_ROBOT(2) → { "Name": "HQ_FC", "Args": ["1","50",…] }. The argument grammar (complete quoted runs admitted, quote-aware comma split) is SiemensCycleCallSyntax's, reused verbatim.
  • Bare-word call — L9810, L_ZYM91, HH_ROBOTOFF, with an optional repeat count L123 P3 → { "Name": "L123", "P": 3 }. Restricted to L-leading or underscore-bearing identifiers on purpose: this syntax runs ahead of FlagSyntax and a generic bare-identifier capture would steal TRAORI-class words before their consumers ever saw them — the same underscore heuristic NamedIdentPattern blessed for assignments (corpus named entities all carry an underscore; controller vocabulary never does). On real Sinumerik any bare identifier can be a by-name call; widening further is a corpus-driven follow-up.

Placement: last of the dedicated statement owners and still before SiemensQuotedStatementSyntax — quoted arguments must reach this capture (paired quotes ride inside the args body; a line whose quotes do not pair falls through whole to the quarantine). ExcludedNames keeps this catch-all from re-owning statements that belong elsewhere (default: WRITE stays with the quoted-statement quarantine; control-flow words are reserved for their future syntaxes). Whole-line anchored: a call mixed with other tokens (L9810 M8) does not match and stays visible in the unparsed warning.

SiemensCycleCallSyntax

Captures whitelisted Siemens cycle calls of the shape NAME(arg1,arg2,…) (or the bare NAME form) from the unparsed line text into a Parsing.NAME sub-object holding the verbatim positional argument strings: CYCLE832(0.05,_ORI_FINISH,0.8) → Parsing.CYCLE832 = { “Args”: [“0.05”, “_ORI_FINISH”, “0.8”] }. Empty slots (,,) stay as empty strings; NAME() yields an empty Args array; a bare NAME yields { “Bare”: true } (a marker — empty sub-objects would be swept by intermediate CleanupParsing calls before the consumer runs). A brand Logic syntax consumes the sub-object (e.g. SiemensPathSmoothingSyntax for CYCLE832, SiemensCycle800TiltSyntax for CYCLE800).

Strictly whitelist-based (CycleNames, default CYCLE832 + CYCLE800) — a generic WORD(args) capture would eat OEM/unknown calls (HQ_FC(...), L_39(...)) whose safe-skip handling is a separate work item, and every unlisted call deliberately stays whole in UnparsedText for the visible UnparsedText--Remaining warning.

Must be placed early in the Parsing bundle — before the assignment and tag-value syntaxes: argument tokens standing after ( or , (_ORI_FINISH, or axis-letter shapes like V1) would otherwise be shredded by NamedVarAssignmentSyntax / tag-value captures. Quoted arguments are supported for CYCLE800's swivel-data-record name (CYCLE800(1,"TC1",…)): the args body admits complete "…" runs (which may hide ) or ,) and the comma split is quote-aware; quotes are kept verbatim in the captured arg string so the consumer distinguishes "0" (name) from 0 (number). A line whose quotes do not pair falls through whole to SiemensQuotedStatementSyntax's quarantine.

SiemensDefStatementSyntax

Consumes Sinumerik variable declarations (DEF REAL _X_HOME=155.5, _SAFE_Z / DEF INT N_ST=3) and lowers each declared name into Parsing.Assignments: initialised names carry their initialiser expression text (the Evaluation bundle resolves and routes it into Vars.Named like any named assignment), bare names carry “0” — Sinumerik default-initialises numeric types to zero, so a later G0 Z=_SAFE_Z reads 0 instead of failing vacant.

Must run before NamedVarAssignmentSyntax in the Parsing bundle: on a DEF line that syntax would capture _X_HOME=155.5 but strand DEF REAL (and the comma list) as unparsed residue. Initialiser expressions are delimited by the SiemensExpressionParser longest-valid-prefix rule, so parenthesised arithmetic and whitespace survive; a non-numeric type or unparsable initialiser leaves the whole statement untouched (visible as residue) rather than half-consuming it.

BOOL/CHAR initialise to 0 like INT; STRING declarations are not lowered (string values have no numeric store) — the statement is consumed whole so it cannot shed ghost tokens. The declaration itself leaves no Parsing record: the lowered Assignments entries carry the entire semantic payload, and an inert record would only re-surface as an Unconsumed residue.

SiemensFrameStatementSyntax

Captures a whole Siemens frame statement (TRANS/ATRANS/ROT/AROT/ ROTS/AROTS/CROTS/SCALE/ASCALE/MIRROR/AMIRROR, bare or with arguments) into a Parsing.<keyword> sub-object. Sinumerik requires frame instructions in a block of their own, so the whole remaining line belongs to the statement.

The consumed keywords (TRANS/ATRANS/ROT/AROT/ROTS/AROTS — see SiemensProgrammableFrameSyntax) are captured structured: each argument token becomes a sub-key — glued axis words (X0, Y90.) and equals forms (RPL=45, Z=R5) both supported. Literal values are stored as JSON numbers; non-literal right-hand sides stay strings so VariableEvaluatorSyntax resolves them in the Evaluation stage (AROT Z=R5 → Parsing.AROT = { "Z": "R5" }). A token outside the modeled grammar makes the whole statement fall back to the verbatim StatementKey capture — never a half-consumed line.

The remaining keywords (CROTS/SCALE/ASCALE/MIRROR/AMIRROR — zero corpus occurrences, no simulation) keep the verbatim { "Statement": "<args>" } quarantine; the Logic consumer reports them as recognized-but-not-simulated. Either way the axis words are kept out of reach of the quote/context-blind tag-value syntaxes below, which would otherwise mint them into Parsing.X/Y/Z and let ProgramXyzSyntax turn a frame statement into a very real ghost rapid. Deferred-but-unmodeled must mean "no transform yet", never "extra motion".

Must be placed early in the Parsing bundle — after BlockSkipSyntax, before every assignment / tag-value syntax (the ROT RPL=45 shape would otherwise feed NamedVarAssignmentSyntax).

SiemensGotoParsingSyntax

Captures Siemens GOTOF/GOTOB jump statements whole into Parsing.SiemensGoto. Four forms are recognised, mirroring the Fanuc phrase-level split (IF ... GOTO is one phrase, not an IF composed with a GOTO):

  • GOTOF <label> / GOTOB <label> — unconditional forward / backward jump.
  • IF <cond> GOTOF <label> / IF <cond> GOTOB <label> — the Sinumerik single-line conditional jump (no brackets around the condition).

Placement: inside the whole-statement owner group, after SiemensRepeatParsingSyntax and before SiemensIfParsingSyntax — the IF-jump forms carry a strictly longer prefix than the block IF <cond> phrase, so this owner must win first (the block-IF owner also refuses GOTOF/GOTOB tails defensively). Must run before the assignment syntaxes: IF R1==1 GOTOF LBL1 would otherwise be shredded by the assignment capture (its LHS grammar matches R1 and the first = of ==).

The label is captured as written (LBL1, MARKE_A, or an N<num> block number — both are legal Sinumerik jump targets). The condition string is left for VariableEvaluatorSyntax's pass-2 tree walk; SiemensGotoSyntax consumes the section.

SiemensIfParsingSyntax

Captures the Siemens block-conditional phrases whole into Parsing.SiemensIf: IF <cond> (no brackets on Sinumerik), ELSE, ENDIF. The single-line jump form (IF <cond> GOTOF <label>) belongs to SiemensGotoParsingSyntax, which runs immediately before this owner; a GOTOF/GOTOB tail is additionally refused here so an ordering drift degrades to a visible unparsed warning instead of a silently mis-captured block-IF (the Fanuc (?!GOTO\b) precedent).

Placement: whole-statement owner group, before the assignment syntaxes — a condition like R1==1 would otherwise be shredded by the assignment capture (LHS grammar matches R1 plus the first =). The condition string is left for VariableEvaluatorSyntax's pass-2 walk; SiemensIfSyntax consumes the section and decides the branch.

SiemensLabelSyntax

Consumes a Siemens label at block start (LBL1: — identifier followed by a colon, optionally after the N head index) into a block-root SiemensLabel record. Labels are passive jump targets: in the normal flow the record is pure bookkeeping (no motion, no state), while SiemensRepeatSyntax uses this same syntax as its LabelScanUtil probe and matches candidates on SiemensLabel.Name. Writing the record at the block root (not under Parsing) keeps the passive token out of UnconsumedCheckSyntax's audit without needing a Logic-layer consumer — mirroring how SiemensDefStatementSyntax leaves no Parsing.Def entry behind.

Anchored at the start of the remaining unparsed text, so a label buried mid-line is not captured (Sinumerik allows one label per block, at block start). Text after the colon stays in UnparsedText for the rest of the pipeline — LBL1: G1 X0 executes its trailing motion normally. The probe usage requires this syntax to stay self-sufficient: it reads only UnparsedText and never touches the dependency list.

SiemensLoopParsingSyntax

Captures the Siemens loop-construct phrases whole, one Parsing section per construct family:

  • WHILE <cond> / ENDWHILE → Parsing.SiemensWhile.
  • FOR <var> = <start> TO <end> / ENDFOR → Parsing.SiemensFor. The start expression is stored as a single-entry Init map keyed by the variable name — the evaluator's pass-2 tree walk rewrites values only, never keys, so the loop variable's name survives even when it collides with a set named variable. Literal start/end bounds are written as numeric JSON values directly (writer decides the node type); expressions stay strings for the evaluator.
  • bare REPEAT / UNTIL <cond> → Parsing.SiemensRepeatUntil. Only the label-less form is taken — REPEAT LBL1 ... is the P4 section repeat owned by SiemensRepeatParsingSyntax, which runs earlier in the statement group.
  • LOOP / ENDLOOP → Parsing.SiemensLoop.

Placement: whole-statement owner group, after SiemensGotoParsingSyntax / SiemensIfParsingSyntax and before the assignment syntaxes — FOR R1=0 TO 10 would otherwise be shredded by the assignment capture (R1=0 is a legal assignment shape and the TO 10 tail strands). SiemensLoopSyntax consumes all four sections against its shared loop-frame stack.

SiemensMcallSyntax

Captures a Siemens MCALL modal-call statement whole into a Parsing.MCALL sub-object: MCALL CYCLE81(500.,327.9137,0.5,324.564,,,0,1,0) → { “CycleName”: “CYCLE81”, “Args”: ["500.", …] }; a bare MCALL (modal cancel) → { “Bare”: true }; a parenthesis-less callee (MCALL L123) keeps CycleName without Args. The Logic consumer (SiemensModalCycleSyntax) maps the supported CYCLE8x family onto the shared canned-cycle modal machinery and warns on everything else.

Whole-statement owner: MCALL lines are standalone in the corpus, and owning the full statement keeps the cycle's positional arguments away from the assignment / tag-value syntaxes (same shredding hazard as SiemensCycleCallSyntax, whose argument grammar — including complete quoted runs — is reused verbatim). Must run before SiemensCycleCallSyntax: a whitelisted cycle name after MCALL would otherwise be captured bare-of-context and strand the MCALLL word. A line mixing MCALL with other tokens does not match and falls through whole to the visible unparsed warning.

SiemensMsgSyntax

Consumes a Siemens MSG(“text”) operator display call (or the bare MSG() clear form) from the unparsed line text into a block-root Msg section, and surfaces the text as a Msg–Display system message.

Must be placed before every assignment / tag-value / flag syntax in the Parsing bundle (right after TailCommentSyntax / BlockSkipSyntax): the quoted argument routinely contains NC-shaped tokens (MSG("TOOL=5"), MSG("G54 OK"), MSG("T20 R8.000 CR:2.500")) which the quote-blind consumers downstream would otherwise shred into ghost assignments, flags, or axis words. Consuming the whole call here removes that hazard structurally — the planned fix recorded on NcSyntaxUtil's assignment RHS-boundary notes.

Only the two literal forms are consumed: MSG("...") and MSG(). Non-literal arguments (string concatenation MSG("A"<<R1), variable interpolation) are deliberately left whole in UnparsedText for the visible UnparsedText--Remaining warning — half-eating them would silently lose content.

SiemensOrientationVectorSyntax

Captures the Siemens TRAORI orientation-programming words out of the block text into the block-root SiemensOrientation record and says, once per program, that they are recognized but not simulated (Orientation-Vector–NotSimulated): the tool-axis vector A3=/B3=/C3=, the Euler / RPY angles A2=/B2=/C2= (ORIEULER / ORIRPY), the surface normals A4=/A5=, the cone and intermediate-orientation vectors A6=…A8= — the whole [ABC][2-8]= address family — and the ORI* interpolation / reference-system words (OrientationWords, with a balanced parenthesized argument list such as ORIRESET(0,0,0)). The rotary axes keep their previously programmed positions.

Why a dedicated capture: NamedIdentPattern's one-letter+digits alternative (there for H1=21) matches these addresses too, and NamedVarAssignmentSyntax runs ahead of the axis-word syntaxes — captured there, A3=0.5 became Vars.Named.A3 without any diagnostic, the posture silently frozen (the tool-vector plan's "partially eaten" hazard, found on the Siemens preset 2026-09-29). This syntax therefore sits in the Parsing bundle before NamedVarAssignmentSyntax, next to the CR=/TURN= captures that exist for the same reason, and after SiemensQuotedStatementSyntax so quoted text is never shredded. H1=21 is outside the family and still lands in Vars.Named.

Right-hand sides follow the assignment captures' rule: the lexical boundary (the next word= or end of text) bounds the RHS, then the Siemens expression grammar shrinks it to the longest valid prefix and puts the tail back (C3=1 F1000 keeps F1000 for the feed capture; glued A3=0B3=1 splits). A literal value is recorded as a number, anything else verbatim as a string — the record sits outside Parsing, so the variable evaluator does not rewrite it and the unconsumed-Parsing check does not report it. An address written twice on a block keeps its first value; the repeat goes to Parsing.RepeatedWords (RepeatedWordsKey, the pipeline's first-wins rule) and surfaces as Parsing--Unconsumed.

The family is owned whole. A word of it that cannot be recorded is not left for NamedVarAssignmentSyntax — there it would draw the words after it into a failed right-hand side, or land in Vars.Named — but set aside under Parsing.SiemensOrientation, which nothing consumes: the unconsumed check names it (Parsing--Unconsumed, SiemensOrientation.A3) and its neighbours stay where they are. That covers a right-hand side no prefix of which parses (A3=1.2.3, A3=(R1+1; set aside up to the next blank) and an address without a value, recognized by what follows the =: nothing, another assignment (A3= B3=1), an NC address word (A3= X10, A3= X-10.5 — one letter is never a Sinumerik identifier, whose first two characters are letters or underscores; R1 is the R parameter and is a value), or a bare word standing apart from the = (A3= TRAFOOF). The last rule errs on the visible side: a named variable written after a blank (A3= MYVAR) is refused as well and shows up as residue, while A3=MYVAR, A3= _MYVAR, A3= MYVAR*2 and A3= SIN(30) are values. Not of the family, and left in the text: an address or ORI* word that is the tail of an identifier or follows $ (_P1A3=5 is the variable _P1A3; a glued address chain G1X10A3=1 is not an identifier), and an ORI* word whose parenthesis never closes.

Once per program: the notice is gated by a flag on this instance, set when the notice fires and re-armed at a program boundary — the first block of a stream (no block before it: a fresh session pipeline, also on a runner reused across sessions), a TapeBoundary block (a % line starts a new program) or a change of NC file (a session may play several files in a row). The latter two are judged on the program's own blocks only. Inlined pieces never count: the pieces of a called subprogram (stamped SiemensCall and a pushed CallStack by SiemensSubProgramCallSyntax at splice time, the Sinumerik archive header %_N_L6_SPF a callee file may start with included) and the pieces of a REPEAT section (IsInlinedContext(JsonObject); re-segmented inside a callee they carry the callee's file and a clone of the host's CallStack, propagated by SiemensRepeatSyntax, but no SiemensCall record). A program that calls such a subprogram a hundred times still gets one notice. A program is told from the next by those three marks only: the same file played again within one session and without a tape boundary — or two inline plays under the same placeholder path — is one program to this rule, and its notice is the one already among the session's diagnostics. The syntax re-arms itself rather than through ISessionResettable: the runner's session-reset sweep covers the top-level syntax list, and this syntax sits inside the Parsing BundleSyntax. A flag instead of a walk back through the block list: frozen pieces re-parse their JSON on every read, so a walk across a long three-axis section would have cost one parse per block at the first vector block after it.

The plan's P4 reads Words to write the brand-agnostic ToolOrientation / SurfaceNormal sections; that step extends this class rather than adding a second one. A project saved before this syntax existed carries its own serialized syntax list without it (no legacy back-fill, by the policy stated on SoftNcRunner.ApplyLegacyVersionPatches): it keeps the old behaviour until its controller is set again from the Siemens preset.

SiemensProcSyntax

Consumes a Siemens PROC subprogram declaration header (PROC MY_SUB, PROC CONTOUR(REAL X, INT N) SAVE DISPLOF) whole into a block-root SiemensProc record. The header carries no executable state for the simulation — parameter binding to caller arguments is a later work item — but leaving it unparsed would shred the parameter list into ghost axis words the moment the tag-value syntaxes run, so the whole statement is owned here. The verbatim remainder after the name is preserved on Statement for cache-dump readers. Writes at the block root (no Parsing entry) following the SiemensDefStatementSyntax precedent — a record with no Logic consumer must not land in the UnconsumedCheckSyntax audit tree.

Anchored at statement start: Sinumerik requires PROC to be the first statement of a subprogram file, so a mid-line PROC is left alone (visible unparsed warning).

SiemensQuotedStatementSyntax

Quote guard: any line text still carrying a double quote after the literal-form consumers (SiemensMsgSyntax, SiemensToolCallSyntax, SiemensCycleCallSyntax) have run is quarantined whole into Parsing.QuotedStatement = { “Statement”: “<verbatim>” } — WRITE(ERROR,“CUTTIME”,“T5 X100”), string-concatenation MSG("..."<<R1), quoted-argument cycles not yet whitelisted.

Every quote-blind syntax below (flags, numbered flags, tag values, assignments) opens a token boundary at the quote→letter transition, so without this guard the string contents get minted into REAL parser state: "G54" inside a WRITE argument silently switches the active work offset, "T5 X100" mints a ghost tool call plus a ghost motion. Quarantining keeps the whole statement visible as one Parsing--Unconsumed entry (QuotedStatement.Statement) with zero state changes — the same deferred-not-corrupted contract as SiemensFrameStatementSyntax. Proper consumers (WRITE, CYCLE800 quoted args) replace this guard per statement family later.

Trade-off, deliberate: on an exotic mixed block the guard also takes the co-resident plain words (a motion word sharing the block with an unconsumed quoted call goes into the quarantine too). Losing them is visible in the warning text; minting false state would be silent — never trade the first for the second. Corpus quoted statements (WRITE, non-literal MSG) are standalone blocks.

SiemensRepeatParsingSyntax

Captures a Siemens REPEAT statement whole into Parsing.SiemensRepeat: REPEAT LBL1 LBL0 → { “Start”: “LBL1”, “End”: “LBL0” }, with an optional trailing repetition count (P=3 or P3). The Evaluation consumer (SiemensRepeatSyntax) re-runs the label-bounded section. The single-label form (REPEAT LBL1 — repeat from the label back to the REPEAT line) is captured with End absent; the consumer currently warns it unsupported (corpus count: zero).

Whole-statement owner, anchored at statement start — mixing REPEAT with other tokens on one block does not match and falls through whole to the visible unparsed warning. The end-label alternative refuses a P+digits word so REPEAT LBL1 P3 reads as start-label + count, not as an end label named P3.

SiemensToolCallSyntax

Consumes the Siemens string tool call T=“name” from the unparsed line text into Parsing.T as a string value (quotes stripped). Numeric concatenated calls (T5) stay with the preset's IntegerTagValueSyntax; ToolChangeSyntax accepts either shape.

Must be placed before every tag-value syntax in the Parsing bundle: tool names routinely contain NC-shaped tokens (T="D16R3Z6" — quote-blind consumers would mint ghost Parsing.D/Parsing.Z entries out of the name; (?<=[0-9]) and quote→letter \b boundaries in RegexFlagPrefix both open inside the string). Same structural-consumption rationale as SiemensMsgSyntax.

A second T="..." behind a ; comment (T="M6LWXD-1301";T="M6LWX", real corpus shape) never reaches this syntax — the quote-aware TailCommentSyntax runs earlier and strips it. Tool names are matched verbatim, including non-ASCII bytes from legacy code pages.