NC (Norsk Data C, A06) — Usage Specification and MON Call Contract¶
Full path of this document: /mnt/d/ND/500/FraTor/nc/nc-a06-usage-and-mon-contract.md
Binary: /mnt/d/ND/500/FraTor/nc/nc-a06.dom — "Norsk Data C - Version: A06 - 1989-01-10"
Date: 2026-07-10
Purpose¶
NC does not currently reach code generation under emulation — every
code-producing phase prints no rewrite / terminated and exits. This
document exists so that NC can be driven correctly, and so that any
remaining misbehaviour can be attributed to the monitor-call emulation rather
than to the way we invoke the compiler.
It is written as a contract. Section 3 is how to drive NC. Section 5 is the exact set of 34 monitor calls NC issues, extracted from the binary, with the argument shapes NC actually passes. Section 6 lists the specific places where a wrong MON implementation would produce exactly the symptom we see.
Companion documents:
/mnt/d/ND/500/nd-linker/nd500-c-compile-and-link.md— the full C build pipeline/mnt/d/ND/500/nd-linker/linker-b01.dom.moncalls.md— how ND-500 MON calls work/mnt/d/ND/500/FraTor/nc/NC-INTERFACE.md— the original probe results/mnt/d/ND/500/FraTor/nc/nc-a06_analysis.md— deep binary analysis
1. Binary facts¶
| Property | Value |
|---|---|
| Format | ND-500 Domain (DOM), SINTRAN III root domain |
| Segments used | 1 (segment 1) |
| Program part | 0x2ECBE bytes, virtual base 0x08000000 |
| Data part | 0x1E4F1 bytes |
| Entry point | 0x08000004 |
| Linker version | v97.251 |
| Instructions (linear sweep) | 44,450 |
| MON call sites | 50 |
| Distinct MON numbers | 34 |
| MON instruction used | CALL (opcode 0xC3) only — no CALLG |
That last row matters: because NC issues every monitor call through CALL and
never CALLG, nd500-dis annotates all 50 of them correctly. (The ND LINKER,
by contrast, uses CALLG for 27 of its 95 sites, which nd500-dis does not yet
annotate.)
The MON wrapper block sits at 0x0802DB00 .. 0x0802E207, one small
ents / marshal / call $0xF8xxxxxx / ret routine per call, plus a few
scattered elsewhere (0x08029027, 0x0802907E, 0x080290A8, 0x0802CCF7,
0x0802D171, 0x0802D1A5, 0x0802D1D8, 0x0802D447, 0x0802D8FE,
0x0802ECAC).
2. How NC talks to the terminal¶
This is the part most likely to be driven wrongly, so get it exact.
- On start NC prints its banner via
MON 504B DVOUTSto device 1:Norsk Data C - Version: A06 - 1989-01-10 - It then reads via
MON 503B DVINST, one character at a time (MaxNo = 1), initially from device 0 — the SINTRAN command buffer, i.e. the initial argument line, not the terminal. - There is no
NC:prompt until the first CR. WhenDVINSTreturns a CR (0x0D) on device 0, NC emits0D 0A 4E 43 3A 20=\r\nNC:and switches its input to device 1 (the interactive terminal). Every command after that is read character-by-character from device 1. - NC also calls
MON 12B SETCM(SetCommandBuffer) at0x0802DB6Ewith a0x80-byte buffer. This is the command-buffer path — it is how the initial argument line reaches the program.
Two correct ways to drive it:
- One-liner via device 0. Queue
COMPILE name,name,name\rbefore the banner. NC consumes it as the initial command line. This is what the ctestdom_nc_compile_*tests do. - Interactive. Queue
\rfirst (that yields theNC:prompt), then the command text:\rcompile B,B,BOUT\r.
Sending an interactive command without the leading \r will be swallowed by the
device-0 path and the prompt will never appear.
3. Command reference¶
From NC's own built-in help. <x: > = a value NC prompts for, [...]
optional, ... repeatable.
| Command | Arguments / prompts |
|---|---|
compile |
<source file: >,<list file: >,<object file: > |
preprocess |
<source file: >,[<list file: >],[<output file: >] |
check |
<source file: >,[<list file: >],[<CAT file: >] |
generate-code |
<CAT file: >,<object file: > |
link |
<source file: >,<program: > |
cross |
<source file: >,<cross reference file: >,<lines per page: > |
format |
<source file: >,<new source file: > |
define |
[<macro identifier [(identifier,...)]: >],[<value: >] |
undef |
[<macro identifier: >] |
directory |
[<include directory/user: >] |
options |
<option: >... (repeatable) |
library |
<library file: >... (repeatable) |
value |
<definitions / options / libraries: > |
page-length |
[<lines: >] |
initialize-compile-parameters |
[<initialization file: >] — reads NC-A:INIT |
save-compile-parameters |
[<initialization file: >] — writes NC-A:INIT |
clear |
(none) — reset compile parameters |
cc |
(none) — silent, returns to NC: |
help |
<command: > (blank = list all) |
exit |
(none) |
@<cmd> |
pass a command to SINTRAN |
File-name handling — a real trap¶
NC treats a dot as part of the name. Typing B.C makes NC look for host
file B.C.C, because it appends the default type itself. Always type the bare
SINTRAN name: B.
Default types NC appends: source :C, listing :LIST, object :NRF,
preprocessed :PP, intermediate :CAT.
NC-A:INIT¶
save-compile-parameters writes it (77 bytes). Contents are the default option
string:
options m2 a4 f- r4 l+ d+ n+ s- p- i- o- pr- ic+ lm+ t- a- lo+
Each token is <flag><+|-|digit>. On start NC always tries to open NC-A:INIT
and normally fails with error -46 (no such file). Pre-placing a valid one
makes the open succeed and changes nothing else — the missing init file is not
the blocker.
4. Observed behaviour of each phase¶
All probes used source GUEST/B.C containing #define VALUE 42 ... x = VALUE;.
| Command | Result |
|---|---|
preprocess B,B,BPRE |
writes BPRE:PP, 36 bytes = correct macro-expanded source, then no rewrite / terminated, then MON 0B LEAVE |
check B,B,BCHK |
prints preprocessing, writes BCHK:CAT, 30 bytes = preprocessed source only, not a parsed intermediate, then terminates |
compile B,B,BOUT |
opens BOUT:NRF, B:LIST, B:C; BOUT:NRF = 30 bytes of preprocessed source; terminates |
generate-code |
never reached when chained — any preceding phase command exits the whole program |
link B,BPROG |
opens SCRATCH-00001:MODE, prints preprocessing, terminates |
cc, clear, options, define, undef, directory, library, value, page-length |
return to NC:, no exit |
Full console transcript for compile B,B,BOUT:
Norsk Data C - Version: A06 - 1989-01-10
NC:
preprocessing
no rewrite
terminated
0:00:01
What is established about the blocker¶
- It is not a MON error. A full MON trace of
checkshows every call (50B OPEN,117B RFILE,120B WFILE,76B SETBS,73B SMAX,62B RMAX,43B CLOSE) returning success. no rewriteis not a literal string in the disassembly; it is assembled at run time and emitted viaMON 32B MSGfromPC 0x0802D199. (0x0802D1CCis the separate newline-MSG site.)- Over 1,113,964 traced instructions: all compiler code is loaded and
exercised, the highest PC reached is
0x0802E506(the top of the code), so the back end is present and reachable. 871k instructions (87%) run in the0x0800Dxxxxlexer/preprocessor region and produce the correct expansion (VALUE→42). The front end works. - The driver at
0x0802Cxxx-0x0802Exxxthen emitsno rewritevia the print routine at0x0802D171without invoking the code generator. - Not caused by: the missing
NC-A:INIT; theo+option (tested); any MON error return.
5. The MON contract — all 34 calls NC issues¶
Extracted from nc-a06.dom by disassembly. args is the argument count encoded
in the CALL instruction. Names are from the authoritative registry
(/home/ronny/repos/nd500x/src/libmon/mon_registry.c) and the 233 YAML specs,
not from nd500-dis's built-in table, which is wrong for several numbers.
| MON | dec | Name | args | sites | wrapper(s) |
|---|---|---|---|---|---|
0B |
0 | LEAVE (ExitFromProgram) | 0 | 5 | 0802D1D8 0802D447 0802D8FE 0802ECAC + entry |
1B |
1 | INBT (InByte) | 2 | 1 | 0802DB0D |
2B |
2 | OUTBT (OutByte) | 2 | 1 | 0802DB40 |
11B |
9 | TIME (GetBasicTime) | 0 | 2 | 0802D1D8 0802D447 |
12B |
10 | SETCM (SetCommandBuffer) | 1 | 1 | 0802DB6E |
30B |
24 | GETRT (GetOwnRTAddress) | 0 | 1 | 0802DBC7 |
32B |
26 | MSG (OutMessage) | 1 | 6 | 0802D171 0802D1A5 0802D1D8 0802D447 0802D8FE |
41B |
33 | ROBJE (ReadObjectEntry) | 2 | 1 | 0802DBDC |
43B |
35 | CLOSE (CloseFile) | 1 | 1 | 0802DC06 |
50B |
40 | OPEN (OpenFile) | 4 | 1 | 0802DC2D |
54B |
44 | MDLFI (DeleteFile) | 1 | 1 | 0802DD13 |
62B |
50 | RMAX (GetBytesInFile) | 2 | 1 | 0802DD76 |
64B |
52 | ERMSG (WarningMessage) | 1 | 2 | 0802D447 0802DDA0 |
73B |
59 | SMAX (SetMaxBytes) | 2 | 1 | 0802DDB7 |
76B |
62 | SETBS (SetBlockSize) | 2 | 1 | 0802DDE0 |
113B |
75 | CLOCK (GetCurrentTime) | 1 | 1 | 0802DE0D |
114B |
76 | TUSED (GetTimeUsed) | 0 | 2 | 0802D1D8 0802D447 |
117B |
79 | RFILE (ReadFromFile) | 5 | 1 | 0802DE4B |
120B |
80 | WFILE (WriteToFile) | 5 | 1 | 0802DE93 |
122B |
82 | RESRV (ReserveResource) | 3 | 1 | 0802DEDB |
123B |
83 | RELES (ReleaseResource) | 2 | 1 | 0802DF03 |
142B |
98 | ERMON (ToErrorDevice) | 2 | 3 | 0802D1D8 0802D447 |
143B |
99 | RSIO (ExecutionInfo) | 4 | 2 | 0802D447 0802DF21 |
221B |
145 | CRALF (CreateFile) | 3 | 1 | 0802DFB5 |
256B |
174 | DEABF (FullFileName) | 3 | 1 | 0802E01A |
262B |
178 | CPUST (GetSystemInfo) | 2 | 1 | 080290A8 |
312B |
202 | MOINF (CheckMonCall) | 2 | 1 | 0802907E |
317B |
207 | UECOM (ExecuteCommand) | 1 | 1 | 0802E116 |
321B |
209 | UEADM (UEAdministrator) | 3 | 1 | 08029027 |
412B |
266 | FSCNT (FileAsSegment) | 4 | 1 | 0802E169 |
413B |
267 | FSCDNT (FileNotAsSegment) | 2 | 1 | 0802E193 |
422B |
274 | GSWSP (GetScratchSegment) | 3 | 2 | 0802CCF7 0802D447 |
503B |
323 | DVINST (InputString) | 14 | 1 | 0802E1CF |
504B |
324 | DVOUTS (OutputString) | 3 | 1 | 0802E1A6 |
Bold = on the critical path for compile, or a call whose semantics are easy to
get wrong.
Note on 321B UEADM¶
The registry marks 321B as deliberately erroring (deprecated). NC calls it
once, at 0x08029027. The C emulator formerly returned success here; correct
behaviour is error 52 with the K flag set. Verify this is still right — if a
handler returns success where NC expects failure, NC may take a different branch.
6. Argument-marshalling contracts, read out of the binary¶
These are the shapes NC actually passes. If the emulator's handler disagrees
with any of them, that is a bug — and several are exactly the sort of bug that
would silently route the driver into no rewrite.
MON 117B RFILE — wrapper 0x0802DE4B¶
0802DE56: w comp2 b.0x20,b.0x28 ; compare two counts
0802DE59: if >= go $0xA
0802DE5C: w1 := b.0x20 ; take the smaller ...
0802DE5E: w1 =: b.0x38
0802DE63: w1 := b.0x28 ; ... of the two
0802DE65: w1 =: b.0x38
0802DE6D: h wconv b.0x16,b.0x2C ; FileNo : halfword -> word
0802DE72: h wconv b.0x1A,b.0x30 ; WaitFlag : halfword -> word
0802DE77: h wconv b.0x26,b.0x34 ; BlockNo : halfword -> word
0802DE7C: call $0xF800004F,$0x5, b.0x2C, b.0x30, @b.0x1C, b.0x34, b.0x38
; MON 117B RFILE(FileNo, WaitFlag, @Buff, BlockNo, NoOfBytes)
0802DE88: if -k go $0xA ; K flag set => error
MON 120B WFILE at 0x0802DE93 is byte-for-byte the same shape.
Spec (from the YAML):
[I] FileNo (INTEGER2)[I] WaitFlag (INTEGER2)— 0 = suspend until complete[O] Buff (BYTES)— must start on an even byte address[I] BlockNo (INTEGER2)— -1 = next block[I] NoOfBytes (LONGINT)— a full 32-bit byte count
Three things to check in the emulator:
NoOfBytesis a byte count, 32 bits, and ismin(requested, available)as computed by NC itself. The classic ND-100RFILE/WFILEtake a word count. CHECKED 2026-07-10: the C emulator already treats it as bytes (fread(buffer, 1, num_bytes, ...)). Not a bug there. Still worth checking in C#.BlockNo = -1means "next block". A handler that treats-1as an absolute block index, or as an error, breaks sequential streaming. CONFIRMED BUG, fixed 2026-07-10. Both C handlers readBlockNointo auint32_tand computedblock_no * block_size, so the sign-extended-1became0xFFFFFFFFand every seek went ~2.2 TB past EOF.-1now resumes from the recorded position. The same bug should be checked for in C#.- The spec declares no
[O]transferred-byte-count parameter. If the handler writes one anyway, it is clobberingNoOfBytes(an[I]parameter).
The preprocessing round-trip does WFILE then RFILE on the intermediate.
Correction (2026-07-10): the byte-vs-word hypothesis is disproven for the C
emulator. An earlier draft of this document ranked it first. Reading
mon_117B_ReadFromFile.c shows the count was already handled as bytes. The
BlockNo = -1 defect found alongside it is real and is now fixed, but it
cannot be the cause of no rewrite either: the traced check run showed every
RFILE/WFILE returning success, which means NC was not passing -1 on that
path. There is currently no leading hypothesis backed by evidence.
MON 50B OPEN — wrapper 0x0802DC2D¶
OPEN(FileNo, AccessCode, FileName, FileType).
[IO] FileNo (INTEGER)— 0 on input means "allocate and return a file number". This is an in/out parameter; a handler that only reads it will leave NC without a file number.[I] AccessCode— 0 sequential write, 1 sequential read, 2 random read/write, 3 random read only, 4 sequential read/write, 5 sequential write append, 6/7/8/9 contiguous and RT variants.[I] FileName (STRING)up to 64 chars. If empty, the name is read from the terminal — worth knowing, because an empty name is not an error.[I] FileType (STRING)up to 4 chars, e.g.'SYMB'.
NC builds the string descriptors via the helper at 0x0802E208, which is called
with (@descriptor, r1, r2, buffer, $0x0, $0x7F) — note the 0x7F length cap.
The C# emulator has been observed to throw on an empty filename, where the C
emulator returns error 0x2E (46). Per the spec, neither is right for the
"prompt the user" case. Both are wrong in different ways; the divergence is a
known open item.
MON 503B DVINST — wrapper 0x0802E1CF¶
14 arguments:
DVINST(DevNo, MaxNo, @NoOfBytesRet, @Buff,
BreakStrat, EchoStrat,
BreakT1..BreakT4, EchoT1..EchoT4)
[I] DevNo— 0 = SINTRAN command buffer, 1 = own terminal. NC starts on 0 and switches to 1 after the first CR. Getting this wrong is why theNC:prompt does or does not appear.[I] MaxNo— max bytes before break. NC passes 1: it reads one character at a time.[O] NoOfBytesRet,[O] Buff— both outputs.BreakStrat/EchoStrat= 8 means "use the user-defined table" that follows.- The four
BreakTnwords are a 128-bit break table; a set bit causes a break. The fourEchoTnwords are the echo table; a 0 bit causes echo (note the inverted sense).
MON 504B DVOUTS — wrapper 0x0802E1A6¶
DVOUTS(DeviceNo, NoOfBytes, Buffer). NoOfBytes max 2048. Device 1 = own
terminal; otherwise a SINTRAN open file number.
MON 32B MSG — six sites¶
MSG(Message) — a single [I] STRING, max 512 chars, to the user's terminal.
This is what emits no rewrite (from 0x0802D199) and the newlines (from
0x0802D1CC). NC uses MSG for diagnostics and DVOUTS for the banner.
MON 76B SETBS, 73B SMAX, 62B RMAX¶
SETBS(FileNumber, BlockSize)—BlockSizemust be even; factors of 2048 are most efficient. NC sets 2048.SMAX(FileNumber, MaxBytePointer)—INTEGER4, a byte pointer.RMAX(FileNumber, @NoOfBytes)—[O] INTEGER4, a byte count.
All three are byte-denominated on the ND-500. Same word-vs-byte trap as
RFILE/WFILE.
MON 312B MOINF¶
MOINF(MonCallNumber, @MonCallEntry). Returns the address of the monitor-call
entry, 0 means not implemented. Both emulators correctly return a fake entry
address of 0xF8000000 + n for implemented calls. A C-side test expecting 0/-1
is the test being wrong, not the handler.
MON 412B FSCNT / 413B FSCDNT¶
FSCNT(FileNo, LogSegmentNo, AccessType, @SegmentNo) — map a file as a segment.
LogSegmentNo = 0 means "pick the first free one" and the choice comes back in
[O] SegmentNo.
FSCDNT(FileNumber, LogSegmentNumber) — unmap. Note the spelling: the registry
name is FSCDNT, and the C# emulator registers it as FSDCNT while its
code calls it FSCDNT. Its 97 lines of real logic are dead code because of
the broken registration wiring.
7. Where to look next¶
The single highest-value experiment, unchanged from the earlier probe:
Set an execution breakpoint at
PC 0x0802D199(theMON 32B MSGsite that emitsno rewrite) and walk backwards through the trace. Find the branch that routed the compile into theno rewritepath instead of the code generator, then find the value it tested and where that value came from.
Ranked hypotheses:
- ~~
RFILE/WFILEbyte-count semantics.~~ RULED OUT 2026-07-10. The C handler already treatsNoOfBytesas a byte count (fread(buffer, 1, num_bytes, ...)). A genuineBlockNo = -1bug was found next to it and fixed, but the tracedcheckrun showed everyRFILE/WFILEreturning success, so NC never exercised it either. nc-a06.domis a front-end-only image and expects a separate driver or a genuine two-stepcheck→generate-codewith a real parsed:CAT. Against this: the back end is present and the top of the code is reached. For it: the:CATthis front end writes is just preprocessed source, not a parsed intermediate.321B UEADMreturning success where the spec says error 52 + K flag.- The option string. Ruled out —
o+was tested and changes nothing. - The missing
NC-A:INIT. Ruled out — pre-placing a valid one changes nothing.
A golden reference for what a real NRF file looks like:
/mnt/d/ND/500/FraTor/test-real/test-real.nrf (genuine ND-produced, 1991).
8. Provenance¶
Established by disassembling nc-a06.dom for this document: the 50 MON call
sites, the 34 distinct numbers, the wrapper addresses, the argument counts, the
fact that NC uses CALL exclusively and never CALLG, and the
RFILE/WFILE/OPEN/DVINST/SETCM marshalling sequences quoted verbatim in
section 6.
Taken from the authoritative MON tables (mon_registry.c, the 233 YAML
specs, mon-calls.json, which agree with each other): all parameter names,
types, directions, and the semantics of AccessCode, BlockNo = -1, the break
and echo table conventions.
Taken from the earlier emulator probes (NC-INTERFACE.md): the console
dialogue, the per-command outcomes, the instruction-trace figures, and the list
of ruled-out causes. I did not re-run those probes.
Inference, not fact: that the RFILE/WFILE count semantics are the cause
of no rewrite. It is the best-supported hypothesis, not a finding.