x86-64 machine code, 34 33 bytes
B8 66 41 AD AC AC 24 07 9E 92 66 98 7A 0A 72 03 F6 E2 92 FF CF 7B EA B8 F6 F2 84 E4 74 F4 24 80 C3
Try it online!
Following the standard calling convention for Unix-like systems (from the System V AMD64 ABI), this takes the length of the string (9) in RDI and its address in RSI, and returns a value in AL, 128 if the expression is Python 2/3 safe and 0 if not.
In assembly:
f: .byte 0xB8
This will be explained later.
n: .byte 0x66, 0x41, 0xAD
When jumping to n
, the current number will be in DL.
This is a lodsw
instruction, encoded in a strange way for reasons to be explained later. This loads two bytes from the string into AX, advancing the pointer; in particular, the operation symbol goes into AH.
lodsb
Load another byte (a space) from the string into AL.
lodsb
Load another byte (a digit) from the string into AL.
and al, 7
Take the low three bits, obtaining the numerical value of the digit in AL.
sahf
Set certain flags based on certain bits of AH, which contains the operation symbol. In particular:
PF CF
+ 00101011
* 00101010
/ 00101111
xchg edx, eax
Exchange registers.
cbw
Sign-extend AL (previous number); this makes AH 0.
jp d
Jump if PF=1, which is true only for /
.
jc t
Jump if CF is 1, which is true for +
but not for *
. An addition breaks a sequence of *
and /
, making the previous number irrelevant; the current number is correctly in DL.
Only *
is left here.
mul dl
Multiply AL (previous number) by DL (new digit).
s: xchg edx, eax
Exchange registers. The product goes into DL.
t: dec edi
(+
joins here.) Subtract 1 from EDI.
jpo n
Jump if the sum of the low 8 bits is odd. This is true the first two times, with 8 (1000₂) and 7 (111₂), but not true the third time, with 6 (110₂).
.byte 0xB8
If we get here, the expression is Python 2/3 safe.
This subsumes the next two instructions into mov eax, 0xE484F2F6
.
d: div dl
(Recall that /
jumps here.)
Divide AX by DL, putting the quotient in AL and the remainder in AH.
test ah, ah
Set flags based on the value of AH.
jz s
If the remainder is 0, jump (and next, exchange the quotient into DL and proceed).
Also, the success case joins here. In that case, the zero flag was last set by dec edi
, which gave a nonzero result, so the jump is not taken.
and al, 0x80
Keep the high bit of AL.
In the success case, the high bit is 1 from the mov
.
In the failure case, AL is the quotient, which is at most 12 (from 25/2), so the high bit is 0.
ret
Return.
Now, the start can be explained. The first byte 0xB8 subsumes the next four bytes into mov eax, 0xACAD4166
. Those bytes include a lodsw
instruction, which was encoded with a nonfunctional REX.B prefix (0x41) so that that value goes into AH. After that, the first digit is read by lodsb
. The 0x41 value, which is in the same place as an operation symbol would be in later iterations, has a 1 in bit 0 and a 0 in bit 2, so it gets treated in the same way as a +
, making the first digit alone become the current number.
4/2
still gives different types in Python 2 vs Python 3. For example, you can writex = 4 / 2; print('string'[x]);
in Python 2, but not Python 3. \$\endgroup\$