you can of course build an associative array in any language, especially if you're satisfied with the linear search used by lisp alists. you can hack one together in a few minutes almost without thinking:
.intel_syntax noprefix
lookup: push rbx
push rbp
mov rbx, rdi # pointer to alist node pointer
mov rbp, rsi # key
2: mov rdi, [rbx] # load pointer to alist node
test rdi, rdi # is it null?
jnz 3f # if not null, skip not-found case
mov rax, rbx # pointer to null pointer is return value
xor rdx, rdx # but associated value is null (sets zero flag)
jmp 4f # jump to shared epilogue
3: mov rdi, [rdi] # load pointer from key field of alist node
mov rsi, rbp # reload key from callee-saved register
call comkey # sets zero flag if rsi and rdi are equal keys
jne 1f # skip following return-value case code if !=
mov rax, rbx # pointer to node pointer is return value
mov rdx, [rbx] # also let’s follow that pointer to the node
mov rdx, [rdx + 16] # and return its value field too in rdx
test rax, rax # clear zero flag (valid pointer is not null)
4: pop rbp
pop rbx
ret
1: mov rbx, [rbx] # load pointer to alist node again
add rbx, 8 # load pointer to next-node pointer field
jmp 2b
(untested, let me know if you find bugs)but that's very different from having them built in as a language feature, like snobol4, mumps, awk, perl, python, tcl, lua, and js do. the built-in language feature lisp had for structuring data 20 years earlier was cons, car, and cdr, which is not at all the same thing
other programs on unix that contained associative arrays prior to awk included the linker (for symbols), the c compiler, the assembler, the kernel (in the form of directories in the filesystem), and the shell, for shell variables. none of these had associative arrays as a programming language feature, though. the bourne shell came closest in that you could concatenate variable names and say things like
eval myarray_$key=$val
here's the implementation of binary tree traversal for setting shell variables in the v7 bourne shell from 01979 NAMPTR lookup(nam)
REG STRING nam;
{
REG NAMPTR nscan=namep;
REG NAMPTR *prev;
INT LR;
IF !chkid(nam)
THEN failed(nam,notid);
FI
WHILE nscan
DO IF (LR=cf(nam,nscan->namid))==0
THEN return(nscan);
ELIF LR<0
THEN prev = &(nscan->namlft);
ELSE prev = &(nscan->namrgt);
FI
nscan = *prev;
OD
/* add name node */
nscan=alloc(sizeof *nscan);
nscan->namlft=nscan->namrgt=NIL;
nscan->namid=make(nam);
nscan->namval=0; nscan->namflg=N_DEFAULT; nscan->namenv=0;
return(*prev = nscan);
}
if you're not sure, that's c (now you know where the ioccc came from)