CP-1610 machine code, 20 17 DECLEs1 ≈ 22 bytes
As per the exception described in this meta answer, the exact score is 21.25 bytes (170 bits)
A routine expecting the input number in R0 and returning the result in R3.
1D2 | CLRR R2
1C9 | CLRR R1
0D1 | @@loop ADDR R2, R1
00A | INCR R2
084 | MOVR R0, R4
10C | @@sub SUBR R1, R4
10C | SUBR R1, R4
114 | SUBR R2, R4
22E 004 | BGT @@sub
20C 001 | BNEQ @@next
093 | MOVR R2, R3
141 | @@next CMPR R0, R1
226 00D | BLE @@loop
0AF | JR R5
How?
The CP-1610 has no multiplication, no division, no modulo. We want to implement an algorithm that relies on additions and subtractions exclusively.
We start with \$k=0\$. At each iteration, we update \$j\$ in such a way that:
$$j = \frac{k(k-1)}{2}$$
The good thing about this formula is that it's very easy to compute iteratively: we just need to add \$k\$ to \$j\$ and increment \$k\$ afterwards.
In order to test whether \$n\$ is divisible by \$k^2\$, we initialize a variable \$x\$ to \$n\$ and subtract \$k^2\$ until \$x\le 0\$.
We do not explicitly store \$k^2\$, but it can be easily obtained with:
$$2j+k=k(k-1)+k=k^2$$
Each time we end up with \$x=0\$, we update the final answer to \$k\$.
We stop when \$j\$ is greater than \$n\$.
Here is a link to an implementation of the algorithm in low-level JS.
Full commented test code
ROMW 10 ; use 10-bit ROM width
ORG $4800 ; map this program at $4800
PNUM QEQU $18C5 ; EXEC routine: print a number
MULT QEQU $1DDC ; EXEC routine: signed multiplication
;; ------------------------------------------------------------- ;;
;; main code ;;
;; ------------------------------------------------------------- ;;
main PROC
SDBD ; set up an interrupt service routine
MVII #isr, R0 ; to do some minimal STIC initialization
MVO R0, $100
SWAP R0
MVO R0, $101
EIS ; enable interrupts
MVII #$200, R3 ; R3 = backtab pointer
SDBD ; R4 = pointer to test cases
MVII #@@tc, R4
@@loop MVI@ R4, R0 ; R0 = next test case
TSTR R0 ; stop if it's 0
BEQ @@done
PSHR R4 ; save R4
PSHR R3 ; save R3
CALL pSquare ; invoke our routine
MOVR R3, R0 ; copy the result into R0
PULR R3 ; restore R3
CALL print ; print the result
PULR R4 ; restore R4
B @@loop ; go on with the next test case
@@done DECR R7 ; done: loop forever
;; test cases
@@tc DECLE 4, 9, 12, 13, 108, 300, 800, 900
DECLE 0
ENDP
;; ------------------------------------------------------------- ;;
;; prints the result of a test case ;;
;; ------------------------------------------------------------- ;;
print PROC
PSHR R5 ; save the return address on the stack
MVII #4, R1 ; R1 = number of digits
MOVR R3, R4 ; R4 = backtab pointer
ADDI #5, R3 ; advance by 5 characters for the next one
PSHR R3 ; save R3
CLRR R3 ; R3 = attributes (black)
CALL PNUM ; invoke the EXEC routine
PULR R3 ; restore R3
PULR R7 ; return
ENDP
;; ------------------------------------------------------------- ;;
;; ISR ;;
;; ------------------------------------------------------------- ;;
isr PROC
MVO R0, $0020 ; enable display
MVI $0021, R0 ; color-stack mode
CLRR R0
MVO R0, $0030 ; no horizontal delay
MVO R0, $0031 ; no vertical delay
MVO R0, $0032 ; no border extension
MVII #$D, R0
MVO R0, $0028 ; light-blue background
MVO R0, $002C ; light-blue border
MVO R0, $002C ; light-blue border
JR R5 ; return from ISR
ENDP
;; ------------------------------------------------------------- ;;
;; our routine ;;
;; ------------------------------------------------------------- ;;
pSquare PROC
CLRR R2 ; R2 = k
CLRR R1 ; R1 = k(k - 1) / 2
@@loop ADDR R2, R1 ; add R2 to R1
INCR R2 ; k++
MOVR R0, R4 ; start with R4 = n
@@sub SUBR R1, R4 ; subtract 2 * (k(k - 1) / 2) = k² - k
SUBR R1, R4 ; from R4
SUBR R2, R4 ; subtract k from R4
BGT @@sub ; until R4 is less than or equal to 0
BNEQ @@next ; did we reach exactly 0? ...
MOVR R2, R3 ; ... yes: update R3
@@next CMPR R0, R1 ; go on while R1 is less than or
BLE @@loop ; equal to R0
JR R5 ; return
ENDP
Output
This is the output for the following test cases:
4, 9, 12, 13, 108, 300, 800, 900

screenshot from jzIntv
1. A CP-1610 opcode is encoded with a 10-bit value (0x000 to 0x3FF), known as a 'DECLE'.
a√b
. \$\endgroup\$ – xnor Jul 8 '20 at 10:28n=0
is an exceptional edge case that would be better to disallow, since 0 can be expressed as0√b
ora√0
for anya
orb
. And if we consider simplest form to mean takinga
as big as possible ina√b
, there's no limit on how biga
can be. \$\endgroup\$ – xnor Jul 8 '20 at 10:37a
if there is more-than-onea√b
solution (as implied by xnor), or whether anya
is Ok. If the former, I'll need to add one more byte to my answer. Suggest test case 108 to clarify (√108 = 2√27, and 3√12, as well as 6√3). \$\endgroup\$ – Dominic van Essen Jul 8 '20 at 12:00108
is a nice additional test case. If either the lowest or highest (or any) \$a\$ would be allowed, an output of1
would suffice for every possible input, since \$1\sqrt{b}\$ would be valid - in which case I could have a 0-byte solution in Retina. ;) \$\endgroup\$ – Kevin Cruijssen Jul 8 '20 at 12:29