The 6502 uses the carry bit system (over a borrow bit). On such machines, the bitwise NOT is applied to the subtrahend before it is "subtracted" (really, added, taking advantage of two's complement) from the minuend. Since obviously when you're generating machine from assembly code, you have control over the bytes which are actually emitted, for 6502 variants like the NES's 2A03, was the presence of both ADC and SBC by immediate necessary? Couldn't the immediate just be NOTed at the assembly phase? Or are the flags reckoned differently for ADC and SBC?
No, ADC and SBC does exactly the same except SBC does a NOT operation on the argument before adding, and when it comes to immediate values, they are definitely redundant.
However, when it comes to other addressing modes, ADC and SBC instructions were both needed. And as the 6502 is pretty orthogonal when it comes to instructions and addressing modes, this is why both SBC #imm exists even though it could always be replaced with ADC #~imm.
EDIT: The overflow flag, which is "simply" the XOR of the 6th and 7th bit carry, is the same whenever ADC and SBC is used.
I'd also say that pseudo-instructions, while common in the ARM world for example, were unknown/uncommon during the 6502 days. Back then people expected instructions to be in memory exactly what they typed in the assembly code.
Conclusion: The decimal mode is the only way and
ADC #~imm and
SBC #imm will make a difference. However, while the decimal mode rarely used and marginal today in hindsight, especially considering it was removed for the NES/Famicom, you should not forget that it was a major marketing tool back in the late '70s, which was a selling point for this CPU.
was the presence of both ADC and SBC by immediate necessary?
Necessary is for instruction sets always a misleading question. After all, next to all instructions could be omitted from the hardware and put into code, essentially ending with a one instruction machine. The question is not about a minimizing the instruction set (including addressing variants) to an absolute minimum but rather to find a (*1) the sweet spot between bloated and unusable.
Couldn't the immediate just be NOTed at the assembly phase?
Sure, it could. But were to put it? But that would require either a(nother) temporary register to store the value after the 'NOTing' instruction to hold it for the 'ADDing' as the accumulator already holds the first operand. In addition the 'NOTing' instruction would have to have all addressing modes of the
SBC instruction it replaces to be as useful. This means as many opcodes and decode-ROM entries with SBC - plus an additional one for 'Add Temp to A' (*2).
Now if this question is strictly only about dropping the SBC with an immediate value, then yes, it would have saved a single opcode, but no hardware at all, as the implied negation would still need to be there for all SBC with a memory operand. Maybe one line in the decode-ROM could be saved - but at horrible cost with potential buyers ... 'Wut? A CPU that can't subtract an immediate value? No way I'd buy that'
Bottom line: While removing SBC is a nice Gedankenexperiment, is will increase hardware requirement and bloat the instruction set when done general, or have no meaningful savings at all.
Next, the notion of 'at assembly phase' carries a lot of hindsight. Back when the 6500 was done, it's designated assembler was barely than a tool to replace a combination of mnemonic and operand markers into a single opcode. That beast was really dumb. Not to mention, that assembling from hand was considered a normal way to do it. Woz did write not only his monitor, but as well the first Integer BASIC exactly that way - no assembler involved.
P.S.: Instead the 6500 design saved on a not instruction, as it can simply be synthesized by an
EOR #$FF instruction.
*1 - There isn't a single sweet spot, but many, depending on targets and resources.
*2 - Turning this into a Load-Temp instruction and a Neg-And-Add-Temp will lead to the same result.
Many 6502 programs exploit the ability to patch program instructions at runtime. If a program will have to perform a subtraction within a loop, being able to say, e.g.
; Compute value in X somehow, then... stx subPatch+1 ; Now compute a different value in A and set carry subPatch: sbc #0 bcs subPatch
is more convenient than it would be if code had to XOR the value with $FF before storing it.
Note also that the processing "SBC #imm" involves two sets of logic: the immediate-operand-fetch logic which is shared with the six other valid opcodes sharing bit pattern (xxx0 1001) and the "subtract with carry instruction" logic which is shared with the seven other opcodes having the bit pattern (111x xx01). There are some instructions in the memory map which have their own "oddball" logic (for example, seven immediate-mode instructions have the bit pattern xxx01001 which is shared by no other instructions, three likewise have 1xx0 0000, two have 1x10 0100, and has xxxx 0010), making opcode E9 behave as "SBC #imm" requires less circuitry than would having it behave in any other consistent fashion.