[Question]
  [
Imagine a very simple language. It has just 2 syntax features: `()` indicates a block scope, and any word consisting only of 1 or more lower case ASCII letters, which indicates a identifier. There are no keywords.
In this language, the value of identifiers is not important except when they appear multiple times. Thus for golfing purposes it makes sense to give them names that are as short as possible. A variable is "declared" when it is first used.
The goal of this challenge is to take a program, either as a string or as a ragged list, and make the identifiers as short as possible. The first identifier (and all its references) should be re-named to `a`, the next `b` then so on. There will never be more than 26 identifiers.
Each set of `()` encloses a scope. Scopes can access variables created in the parent scope defined before but not those created in child or sibling scopes. Thus if we have the program `(bad (cab) (face))` the minimum size is `(a (b) (b))`. A variable belongs to the scope when it is first used. When that scope ends the variable is deleted.
## In summary:
1. If a variable name has appeared in the scope or enclosing scopes before, re-use the letter
2. Else create a new letter inside the current scope
3. At the end of a scope delete all variables created inside the scope.
## Test cases
```
{
    "(rudolf)":                   "(a)",
    "(mousetail mousetail)":      "(a a)",
    "(cart fish)":                "(a b)",
    "(no and no)":                "(a b a)",
    "(burger (and fries))":       "(a (b c))",
    "(burger (or burger))":       "(a (b a))",
    "(let (bob and) (bob let))":  "(a (b c) (b a))",
    "(let (a (fish (let))))":     "(a (b (c (a))))",
    "(kor (kor kor) (kor kor))":  "(a (a a) (a a))",
    "((kor) kor)":                "((a) a)",
    "(aa (ab ac ad) (ad ad) ad)": "(a (b c d) (b b) b)",
    "(aa not (ab ac ad) (ad ad))":"(a b (c d e) (c c))",
    "(((((do) re) mi) fa) so)":   "(((((a) a) a) a) a)",
    "(do (re (mi (fa (so)))))":   "(a (b (c (d (e)))))",
    "((mark sam) sam)":           "((a b) a)",
}
```
## IO
1. You can take input as either a string or ragged array.
2. You can give output either as a string or ragged array.
3. However, you must use the same format for input and output. Specifically, you need to produce output in such a way that it would also be a valid input. Applying the function or program more than once always has the same result as applying it once.
Neither scopes nor variable names may be empty. Applying your program to its result again should be a no-op.
  
[Answer]
# [tinylisp](https://github.com/dloscutoff/Esolangs/tree/master/tinylisp), 149 bytes
```
(load library
(d G(q((L N)(i(c()(h L))(i L(c(G(h L)N)(G(t L)N))L)(i(contains? N(h L))(c(last-index N(h L))(G(t L)N))(G L(insert-end(h L)N
(q((L)(G L(
```
The last line is an anonymous function that takes a nested list and returns a nested list. The returned list uses nonnegative integers instead of letters, which seems to be allowed by [this comment](https://codegolf.stackexchange.com/questions/251864/re-name-all-identifiers-to-a-single-letter#comment560861_251864).
[Try it online!](https://tio.run/##VY07EsMgDER7n2JLqfAZ0oWG8R34JSZDcGwo4tMTwBlP0mj2aVer7OMefHqVQmFRFsHrTW37QBaCViKJicmTIaYZkquGrCQ6VUtQ7oJljy0xKx/TBdM3biiolEcfrXufy/OIRG2rebfl0UV7lLbf1/778Evl7FI2KjnQCgougx7@iSb0ormNvw0UNAwsHG64Y4bnZjHzQNezrMMvc/kA "tinylisp – Try It Online") Or, [verify all test cases](https://tio.run/##bVPLcsMgDLznK3SUDp1pHvfekkum/4ANbphiSIDMNF@fCmHjPswYkMVqvSw4W/9wNl2fT3RBaXC2iyo@aIMaTnhDPMM7ocUeCS9wJo7hzG8neeOlE2YJ6Cyw4LOyPr3B@wTv0amUX6zX5qslWxGemI3xJuYX4/VEWluRcBQJFSbJJ2ozQDYp9yqZtAG8bQAQMd51cAPBSsNXJiszjuGeDAt00CJqIGiwXsUMg00XWuGC7QzzAZTX4MM6bOHr7vHDRMCCHqI1iegHELewo7/QEKGGf6GvDepMBuxCV0RQjTgl@Jl1pUABlp0BCnZmrwW4E69awSerkIE7LdH0hWrZZNxUgYKU4Z8lTL1YoliHYuk9qCKeb16ZS5/Fw17kb2kxnIt8yCuFVA1n/Xs4UJkXR0vTfETREIyWYFAEqR6ZrImq9kxFOgBGAzhatoulcsFs1mLVHvBAP@zCUcVPSGqkOvzefNnJZC4Dr3xznRo7zeTlJhcKNLe7cm9CdgS8GN6crFX@i1zcmuBM@wfo@Q0).
### Ungolfed
```
(load library)
(def _rename
 (lambda (expr names)
  (if (type? (head expr) List)
   (if expr
    (cons
     (_rename (head expr) names)
     (_rename (tail expr) names))
    nil)
   (if (contains? names (head expr))
    (cons
     (last-index names (head expr))
     (_rename (tail expr) names))
    (_rename expr
     (insert-end (head expr) names))))))
(lambda (expr) (_rename expr nil))
```
---
Here's a **167-byte** version that uses letters instead of numbers:
```
(load library
(d G(q((L N)(i(c()(h L))(i L(c(G(h L)N)(G(t L)N))L)(i(contains? N(h L))(c(single-char(a(last-index N(h L))97))(G(t L)N))(G L(insert-end(h L)N
(q((L)(G L(
```
[Try it online!](https://tio.run/##VY6xUsMwEET7fMWWe4Vn7BAIqejixpN/kCURi1FkYqkgX@9ICjDQ3Oze7ry75MLNu/i5rvSzMvBuXNRy29Cg55UccBI6agonDJI1huz66nLUM1UhQ63NISkX4htO33XN6MLZ20ZPaqGiVzE1Lhj79VM57OUPhn3mZ4JdUmODeZwp3xzrN498zT7ZmLSKFryC3ibww11QxDiPUsa/DRRGaBhYvOOMCU5KJCIbHn9h1RRgC3ZosRV04DarDk/Y4Rkv2OMVB3R51QnaDFjv)
[Answer]
# [Python](https://www.python.org), 79 bytes
```
f=lambda _,**s:[i==[*i]and f(i,**s)or s.setdefault(i,chr(97+len(s)))for i in _]
```
[Attempt This Online!](https://ato.pxeger.com/run?1=jZJNbsMgEIWlLnsK5JVx0277I_kMOYCForGBGgVDBHjRs2STTXqn9jSFoVZiqkj1gueZ-WbmCfv4efgIozWn03kO8vHlaytbDVPPgew2TePfOtW2XaMYGE5krVKOWkf8kxeBCwmzDjE7jK5-fX7QwtSeUiojoYgyZMfy2O-74eCUCbWsu8rN3GpZMUrvL8nJznEiKF1tyFWwhgZwIdWl8mNRMjYVosskMVhX-9m9CxdL3YJIp4Sv2E3MJlkSBaZFQKa3_bKUXcWpfKMD8ET76SWTBbu32UFWFFbGq45uYbKuhkHeCNnogOevW-CXCOVvY7zI8I_-tZv4VNziVCdQJoUiAcWXH4db3BHhJBHGO8rGE1zeUGTA7dNyD1MemZTS_K8tv_IP)
Thanks to @loopy walt.
## [Whython](https://github.com/pxeger/whython), 66 bytes
```
f=lambda _,**s:[s.setdefault(i,chr(97+len(s)))?f(i,**s)for i in _]
```
[Attempt This Online!](https://ato.pxeger.com/run?1=jZLNSsQwEMfBo09RcmrW6tUPWARfo5Rl2iQ2bJpIPhCfxctexGfSpzGZWHYTWbCH_DMzv_lgmvfP1_nNz0YfDh_Bi-u7ryexVbCMDJpdt9m4h97dOO4ZFxCUb2U3zba9v71SXLeOUvoooi9yVBjbyEbqZjfkSt8X04uV2rei7YkNzChBBkovj87FhFgapCJdc2KU0ATWp7iQbq5C2qQAaJYkGmV0DPaZ2xjqV0RYyR0ZzmImyeqoMMU9MqMZ16bDiZ3CZzIATxw_XTJZsXuTJ8iKMtR2kdGvTNaiGOSOkAed8PydFtjRQvmbGBfp_5FfThM_wgxWtRxlkSgCUFz9c5jBHhFOEmHcUR48wfWGIgN2n5o7WHLJpJTmt7a-3h8)
Port of the above.
## [Whython](https://github.com/pxeger/whython), 72 bytes
```
f=lambda a,s={}:s.setdefault(a,chr(97+len(S:={**s})))?[f(i,S)for i in a]
```
[Attempt This Online!](https://ato.pxeger.com/run?1=jZKxTsMwEIYlRp4iymSXwApUiljZO0YZLrFNrDo2sh0hVPVJWLogngmeBvtM1MaoUjP49_m-u_vl-OPrbXj3g9GHw-fkxe3D97OoFYwdgwIqV-_2a3fnuGdcwKQ8gaofLHm8v1Fck8263q1Wbk8pfWoEkdWGCmMLWUhdQJv6_Vz1r1ZqTwRpSjsxo0TZUnp9PBzNFAaAVGVVnARLqAfrY15IN2QpbWICNIsSgmW2m-wLtyHVzIiwkruyPYuZKPNBhinukelMNw9tT-KYPlMBuKL9uElkxm5NcpAUpc3jRUUzM0kXzSBNhGS0x_XPLbBjhPK_MFykv6B-6SZ8JTPY1XKUUaIIQHH5z2EGZwQ4SoDxjpLxCOc3FBiw2zjcwZhaRqU0vbX5Df8C)
My original solution
[Answer]
# [Prolog (SWI)](http://www.swi-prolog.org), ~~81~~ 70 bytes
```
\X,[I]-->[H],{copy_term(X,Z),\(Z,H,I);nth1(I,X,H)},(\X;[]).
a--> \_,!.
```
[Try it online!](https://tio.run/##bZBLT8MwEITv@RXhlkjTSuVxQiD1nfR5rXBXldukJaKJkZMKIcRvL2s7lICIZGd3Pnt2kletjurQKt@y83m9goip1XoUEeFjp17fN1Wq82CFpxDr4AkR4vC@qJ47QYwVovATwXp1Lyhse5Kv@esNrtrnswyEPiXquCd0Q3jc5upUppXMjrhUhJ5jO6kr7LPymdB3SqEgiwSFIgycsj3pQ6ohjLzXWVoSYfgbKQ1XMho5dEwriK3aGjdyFUvMxw0uIcxwCIsYRg6@sKHdeNFPRfwLLBdWt9vEKZKtJA/bQZpxMrFvs6aXA4Wq/jlEmNWm/CT82Tol5BlhLwkl93OHEwWhU4g849A8jZGNvKhv51K/oJQ5uW3JcrfrP/iiQ/B6PVvBN02/XzfX3AwG342Dw2HdCxZu2N8bjRpKxyjj8a8zDRJFDSJuYKabkF4cfwMXon4zmUwMMSnt/Om06Q3/trbnrDbvbPaT1wy4hX9HrrRp53NrJy6OzeUtFn/z8X1x5/6kt1y6KG5ah9pf "Prolog (SWI) – Try It Online")
Instead of using predicates, I use definite clause grammar (DCG) notation.
DCG notation is syntactic sugar in Prolog that allows for more writing more concise code for sequence processing.
The basic way my program operates is that it iterates through the input and output list using DCG notation to bind the first item of the remainder of the input list to `NextItem` and the first item of the remainder of the output list to `ProcessedItem`.
Then the program handles each potential case for `NextItem` and binds `ProcessedItem` to the appropriate value.
Finally, the program attempts to process the following items, or, if none are found, terminates.
I stumbled onto an 11 byte golf of my program when I realized my original code was broken.
The issue was that when parsing a sublist, the inner scope would leak into the outer scope since `InScopeVars` was having new variables placed into it by `nth1/3`, instead of by the intended `append/3`.
I found a way to exploit this behavior though with the `copy_term/2` predicate, which unifies the second argument with a copy of the first argument but with fresh variables (e.g. `copy_term(X,Z)` where `X=[aa|SomeVar]` unifies `Z` with `[aa|SomeNewVar]`).
This allows us to create a copy of the scope to use when handling a sublist where its uninitialized tail can be used without the inner scope leaking, thereby allowing us to use `nth1/3` for both appending and finding the index.
## Ungolfed Code
```
rename_item(InScopeVars),
  [ProcessedItem]
  -->
    [NextItem],
    {
      % Case 1: NextItem is a sublist
      % This predicate sets InScopeVarsCopy to a copy of InScopeVars,
      % but replaces all Prolog variables in it with newly instantiated ones.
      % This means any unifications that occur in the following rename_item call
      % will not affect InScopeVars
      copy_term(InScopeVars, InScopeVarsCopy),
      % DCGs are syntactic sugar for predicates with an additional two arguments:
      % the input sequence and the output sequence.
      % This fails if NextItem is not a list, in which case we'll
      % do Case 2 instead.
      rename_item(InScopeVarsCopy, NextItem, ProcessedItem)
    ;
      % Case 2: NextItem is a variable
      % We unify ProcessedItem with the index of NextItem in the list InScopeVars.
      % Note that since InScopeVars initially uninitialized,
      % it gets unified with [NextItem|UninitializedVariable].
      % On later calls, if NextItem appears in InScopeVars then ProcessedItem is
      % unified with the index of NextItem.
      % Otherwise the head of the uninitialized tail of InScopeVars is set to
      % NextItem, and NextItem's new index in InScopeVars is unified with
      % ProcessedItem.
      nth1(ProcessedItem, InScopeVars, NextItem)
    },
    (
      % Attempt to rename the next remaining item in the list
      rename_item(InScopeVars)
    ;
      % If that fails then we are done!
      []
    ).
rename -->
  % Due to the way the nth1/3 predicate works, passing in an
  % uninitialized variable for the initial scope is in essence passing in an
  % infinite list of uninitialized variables.
  % As we encounter new (not Prolog) variables in the input program they will be
  % unified with the first uninitialized (Prolog) variable in the scope.
  rename_item(_),
  % Once we get the first solution, we use the cut to prevent backtracking
  !.
```
Before I realized that it was permitted to output numbers instead of letters I wrote the following program, which I've modified to use some of the golfs I later found:
## 92 bytes
```
\X,[I]-->[H],{copy_term(X,Z),\(Z,H,I);nth1(N,X,H),C is 96+N,name(I,[C])},(\X;[]).
a--> \_,!.
```
[Try it online!](https://tio.run/##bZDda9swFMXf/Vd4bzY7CfSlMMoGifNh58N5DVUuRbGV1jS2iuwwxtjfnl1JbuqOGSTde37yucd@M/qsn0ftz@p6PewhMhqNfoiU8LvQb7@eOmXqaI/HGIfoESmy@KHpXu6iHHukMZKwasNv919zNLJWUQaRUPwH0WH/ICgeB5LNwsMTvoyvVxkJcyn1@USYxAi4rfWlVZ2szrhVhKlnhTQdTlX7Qki80mjIpkSjCTOvHC/mWRkIK59MpVoizD8jbeBLRguPzqqDOOqjdSNfscR8OeASwg6HcIhh6uErG7qNF31UxD/GceF0t628ItlK8rAC0o6TpTvtWt8uNLr7zyXCpjflp@TPNopQV4STJLTcbz0uNYRREHXFoXkaIxc579@upXlFK2vy247lyST8Hgq2CaZTVyG0TZL0zZGb2ey98XA@73vBQsH@wWIxUKRVlstPdwYkTQdEFLDTbcggy96BD9GfTFYrS2xKN3@9HnojLHt7zurybjYfee2AEqEiX7q0262zEzfH4Qry/N98/L5Q/k8Gu52P4qdJGv8F "Prolog (SWI) – Try It Online")
[Answer]
# [Prolog (SWI)](http://www.swi-prolog.org), 101 bytes
This version uses 1-26 instead of a-z (alternative version for a-z below).
Had to (re)learn about Prolog dictionaries for this one, I like them!
```
[H|T]+[I|U]+A/N:-(H@>[],H+I+A/N;I=A.get(H)),T+U+A/N;I is N+1,T+U+A.put(H,I)/I.
A+A+_.
A+B:-A+B+_{}/0.
```
[Try it online!](https://tio.run/##dVJda8IwFH3WX3ERhi2JdXt1KHN7sS8@bApCCJK2qQbbRpIWB3O/3d00tBsbK@Tk5pz7lZuejS70YWIv6nZjq@uGExZft5wsp@vZJFg9LRinKxK782M8X0YHWQerMKQbsvUcKAtr8uCJ6NygTONwGkfDJVmSvdueZxMEsv/4nN5Ht9kELkbVsqiC8UbaWlWHKIrGIR0CQCnOhbJ1wHZ8sQh25FXapqgpDAcD8HZP3cHLUaYnUDnkwXsIc9ycEcJRF5ltQ7pCPgbbBlcEGEtERlkqEk5ZLlLJOfWCaTJd5N2p1I2VtVAFhd7stFQYbCJX9tgxlaYgqoxCpTsqacxBGgqs5XOjpOV/NI3gD71WSMzNEp20GXlnI/3LReByPdCW4LyXTy6rRwT@0@5cmBda9Ixw6YQrmuJq6@KcvOXg26vS9X@ufXr8MpwEGIlQKoRcINh@OhkOjBmJUCp3D1cd1R@3YKUwJwwRJffI/W/SvevorUlTaUdhdPsC "Prolog (SWI) – Try It Online")
(The `H@>[]` part is a bit hacky, it's a list check (`is_list(H)`) that works on TIO, but there's probably a way to get rid of it, as neither are necessary in my ungolfed solution. Please correct me!)
### Ungolfed, commented
```
f([H|T],[NewH|NewT],Dict,N):-
% Case 1) No dict update needed
  ( f(H,NewH,Dict,N); % First element is a list (matches f([H|T]... or f([]...)
    NewH=Dict.get(H)), % First element is an atom
  f(T,NewT,Dict,N);
% Case 2) If first part failed, need to update the dictionary
  NewH is N+1,
  NewDict = Dict.put(H,NewH),
  f(T,NewT,NewDict,NewH).
f([],[],_,_).
% Wrapper predicate for recursive predicate
A+B:-f(A,B,_{},0).
```
### a-z solution, 114 bytes
```
[H|T]+[I|U]+A/N:-(H@>[],H+I+A/N;I=A.get(H)),T+U+A/N;Z is N+1,name(I,[Z]),T+U+A.put(H,I)/Z.
A+A+_.
A+B:-A+B+_{}/96.
```
[Try it online!](https://tio.run/##dVJdS8MwFH12v@IiyFqSdfgiONlw@rK@7EEnyEIYaZtuYW0zkpYJzt8@bxpbRbGQk3vPuR/JTQ9GF3o7skd1PrPFacUJi08vnMzHy8koWNzPGKcLEjv/Lp7Oo62sg0UY0hV5abk1KAtLck0rUcogpmzNv8To0GAojcPxOhrMyZxs3PYwGSGQzfvH@PYmOk9GcDSqlkUVDFfS1qraRlE0DOkAAEpxKJStA/bKZ7PglTxJ2xQ1hcHFBXi7p67gcSfTPagc8uAthCluzghhp4vMtildI5@DdwDXBBhLREZZKhJOWS5SyTn1gmkyXeSdV@rGylqogkJvdloqDB4iV3bXMZWmIKqMQqU7KmnMVhoKrOVzo6TlfzSN4J1eKyTWZolO2oq8s5H@FSJwuTPQluC8l/euqkcE/tPuQpgXWvSMcOWEa5riavvinLzl4Duq0vV/oX15/DKcBBiJUCqEXCDYfjoZDowZiVAqdw/XHdUft2ClMHtMESX3yP1v0r3r5XOTptJehtH5Ew "Prolog (SWI) – Try It Online")
[Answer]
# [C (clang)](http://clang.llvm.org/), 208 bytes
```
a[26];b[26];c;d;e;f;g;h;i(*j){for(;f=*j;j++)if(f/47)c=c?:j;else{if(c){for(*j=g=d=0;a[d]*!g;wcscmp(a[d],c)?d++:g++);g||(a[d]=c,b[e=d]=h);putchar(d+97);}for(c=!putchar(f);f==41&b[e]==h;a[e--]=0);h+=f%17-47%f;}}
```
[Try it online!](https://tio.run/##dVPbbuIwEH3nK6ZIrGwCWlqhRYtl9Qf2DyAPji9JaC7ISdQVNL@@7NhpQoDWSsbjmXPGc5HlUmaiiC8XsXv5FbLIS8kU08ywmCUsJfMDPZvSEmb4/MAOQUBTQ8zP9YZKLl@3B6azSp/RJjvc/MBjrviKiZ0K508xe5eVzI/EHReSvqog2MYYhcUfH97I5SLaaY5KQtmxqWUiLFHB7w1lrQso@VNvNRSz4OvnH0gIOU/wDr1chnxFWRJwM3veLNebmWFte0mLGnKRFoTCeQK40DCHWlf1LgT@aXPrz5TYRpWZodOFOwjcx868bCpdizSDQeuRcI@VwtZg0ioZENEdoihBFAqK8op4iBI1NtYWiAMam@qKDmASgaTfwEsLnXoDF/fwTNdoLyOXB@00NN1d8T0TAa5AIJ50QyMS3Z1tTHvDxLzAn161K9P1sZN3TOIZTnRYjP7QLOECYLIShCtHKL/jP64HfKU4jId5IL0o6y9CjOaDdSnQ1O0PzXdLlRQs@vOUgsEUq364bvmUh@@WrEogVgPJU2wqZorEL1qqgGj62FaSC/sGlcipF32HXJE396wWsPKHlvnNuHG413HCZ7Bi3ZM4hQxOAX@ho4dxtIgyZDrLqn0BWGHVZPUW8KJPCmUDNiX4ylVzJL1r5Ovj7Av996hlrdUWfMx9McSCAJ77eO2knVz@SZOJuLos3/8D "C (clang) – Try It Online")
-1 byte thanks to [Noodle9](https://codegolf.stackexchange.com/users/9481/noodle9)!
-2 bytes thanks to [ceilingcat](https://codegolf.stackexchange.com/users/52904/ceilingcat)!
-1 byte thanks to [c--](https://codegolf.stackexchange.com/users/112488/c)!
[Answer]
# [Ruby](https://www.ruby-lang.org/), ~~69 65~~ 60 bytes
```
f=->l,*r{l.map{|x|x*0==[]?f[x,*r]:""<<97+(r|=[x]).index(x)}}
```
[Try it online!](https://tio.run/##jVLNbsMgDH6VilPbZdVu06amexDEgRTYUJNQmURiavLsmW2C1vW0HPjy/WDsBBib72Vx9fOprfZwaw@dvt6mNKX9S11L9eFkQl29C3E8vr0@bWGqZVK7g@@NTdu0m@dFSgGjCa0TqpKiC2O0g/atqDZ3hKyzhoFU5@MXC30gqntDgIS0ZoRPCyjIYjjwNgr1YAaCIrDZ2oGdJjSlrLrjZP/JaV65GXrJPicuIZ@RkUE9cszJ4qyIJXNNnRs487p2oc0vy7DGcfDhH7voPHyECVwBLEPnGZxmiPkTmsD1MEKAEZ4zt0aRPCU6Gi5UPuoubydU5QZcN/jv1bz8AA "Ruby – Try It Online")
[Answer]
# [K (ngn/k)](https://codeberg.org/ngn/k), ~~65~~ 54 bytes
```
{$[x~*x;`s$`c$97+(s::?s,x)?x;[t:s;r:o'x;s::t;r]]};s:!0
```
[Try it online!](https://ngn.codeberg.page/k#eJx9kt1qwzAMhe/7FB0UFm+52N1YdNEHKQE5ib2FNjHYLqSM9tl3lPQvaTqDZSH5y5EU2+x3telObx1xWHG5+vp8T0KWrUPaqXVHm5gF8pl77QjRSD7Pj/BePhaLKOTh5DO7BOy2lLDV9Y46OpBX+XERNyn7feV2djmzKGU9F8/BceP2wUR87eZdOdazYM+V2ke2dfiZ0QNXPONax7qtcDzhZhSFS7jY+2/jqaetr01QVy5hTSBL9R/n/Nl94PQstzMRSVeI4OAgokZ6E/jGyYWknw5mL5hSFw4FYZfyT4bohds6FAkzcu709NlM9NLr/UkXwqU9M00MelpSqL9kjf50dT7UXX8sjaNeLtSYa118gNWFIwHZyFHeOKyUK0fsjSJuahirYYJTNCT7Uu/3oAcmAQTT1P3Dx+gAYY3miYtIGIkPXKP9loNuSIwaz0Ue2uNg8j8UqPek)
[Answer]
# Rust, 277 bytes
```
enum N{A(String),B(Vec<N>)};fn f(a:&[N],b:&mut Vec<String>)->Vec<N>{a.iter().map(|j|match j{N::A(j)=>N::A(('a'..='z').map(|i|i.to_string()).nth(b.iter().position(|a|a==j).unwrap_or_else(||{b.push(j.clone());b.len()-1})).unwrap()),N::B(j)=>N::B(f(j,&mut b.clone()))}).collect()}
```
[Playground Link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a3966f982bd29bd1b9546c075f5f8139)
[Answer]
# [JavaScript (Node.js)](https://nodejs.org), 109 bytes
```
s=>s.replace(/\w+|\S/g,m=>m>')'?v[m]=v[m]||Buffer([v[0]++]):(v=m<')'?S.push(v)&&{...v}:S.pop(),m),S=[v=[97]])
```
[Attempt This Online!](https://ato.pxeger.com/run?1=dVPNjtMwED5w61OMKrSx1Wx2hUALLe5KSJw59JhGWjdx2rBxHNlJQGr7JFyqFTwJTwFPw9hu06y2WM146vm--fks__hZqUwcnnL2q23y6_d_pGFzE2lRlzwV5Gb5bbJbLm7WoWRzOQ9ocN_FMmHW7Haf2jwXmsRdfJtMJgmdko7Jjxa0iOrWbEhHr662URR1-ymeqJrQUNJwweKOxR_ukoT6on9f_Z6NRl9WX0XaRKJqdCEM2Y4A15joNlNlTsdTeLnGhNNxeARK1RrR8KKE3utZCIQBNOW6gbwwmwtpLXR1hlYKeJVBpf4HHeZdtXotNBDLyO0U9MyyYLKClL6EKw3efQnnA3gpGjxSK9sQ9R4eeU6f_SIJY3ZaIA7fVzmSSIoId3wiPWJHzuBHz965klXT2zOJOLA1l5TCCkOluE2CnabA7Sw8czt-yD3NAm5KvIzhfSCxUs0FMjL9feA4GQhq96HYdmWKgsaQLCjk2JA53qqPug77X0_MFBAtgMgCVcTOkNRrOFAwAyLoMxWJ5PoRDJfUmWeqWEHsZK7Onka50p95uiEkLqq6bUIQ32t8DQkFNgf7FFJVmQZU22AUGOTE4eisD9XcGAycEIwdU5wQqhRRqdbE4e4hWLZv7m7fBTD17ts0COFYO7ie4x-fKYQTwUIfiE8qMni99e6ePmAXezrzb_lw8Ps_)
## Ungolfed and Explained
```
str => str.replace(
  /\w+|\S/g,                          // replace each variable name or bracket
  m => m > ')' ?                      // if match is a variable
    v[m] = v[m] || Buffer([v[0]++]) : // lookup the new var name, otherwise assign new
    (
      v = m < ')' ?                   // otherwise, if match is opening bracket
        S.push(v) && {...v} :         // push current var dict to stack
        S.pop(),                      // otherwise pop var dict
      m                               // return matched bracket (no-op)
    ),
  S = [                               // initialise stack
    v = [                             // initialise var dict
      97                              // v[0] = char code of next var name
    ]
  ]
)
```
[Answer]
# [Raku](https://raku.org/), 76 bytes
```
sub f($x,%h?){$x~~Str??(%h{$x}//=chr 97+%h)!!$x.map:{f $^a,$_}with %h.clone}
```
[Try it online!](https://tio.run/##TYzdCoMwDEZfJUIllRW929jA@RC7n9Q/KqiV1jGH6Ku7xN0sN4fvy0nG2nXnffevAhopZhWaLFrEvG2PyWWZDA2FNUnS0ji4Xk6hiYJAzHGvx9vSgHhqJfL13U4GQhOXnR3qdff6AyJXgJDeARWQl0NjHUiJXT1RI7GwBRH1UGH0l3kdccGDlUVFAV3NEvbtgUYf8PZ3yRKBHAY5DHIYnh8cs38B "Perl 6 – Try It Online")
[Answer]
# Perl, ~~63~~ ~~60~~ 29 bytes
(+1 for `-p`)
```
s/\w+/$a{$&}||=chr$i+++97/ge
```
Run like:
```
$ perl -pe 's/\w+/$a{$&}||=chr$i+++97/ge' <<<"(rudolf)"
```
or even
```
$ perl -pe 's/\w+/$a{$&}||=chr$i+++97/ge' file
```
### Ungolfed and explained
This is a simple replacement using the powerful `/e` modifier, which instructs perl to **e**valuate the replacement as code. It finds each identifier and checks it against a hash. If the hash is empty, it populates the next available letter, starting with ASCII code 97 (`a`):
```
s/                     # substitute
   (\w+)               # match: 1+ word characters
 /
   $letter{$1} ||=     # use existing letter or else assign …
      chr($i++ + 97)   # … a char w/ value of $i + 97, then increment $i
 /gex;                 # match globally and eval the replacement as code
                       # (this explanation uses /x for comments & spacing)
```
Running through the examples on a file containing each test case on its own line:
```
$ while IFS= read line; do 
    printf "%-30s  %s\n" "$line" \
      "$(perl -pe 's/\w+/$a{$&}||=chr$i+++97/eg' <<<"$line")"
  done < test-cases
(rudolf)                        (a)
(mousetail mousetail)           (a a)
(cart fish)                     (a b)
(no and no)                     (a b a)
(burger (and fries))            (a (b c))
(burger (or burger))            (a (b a))
(let (bob and) (bob let))       (a (b c) (b a))
(let (a (fish (let))))          (a (b (c (a))))
(kor (kor kor) (kor kor))       (a (a a) (a a))
((kor) kor)                     ((a) a)
(aa (ab ac ad) (ad ad) ad)      (a (b c d) (d d) d)
(aa not (ab ac ad) (ad ad))     (a b (c d e) (e e))
(((((do) re) mi) fa) so)        (((((a) b) c) d) e)
(do (re (mi (fa (so)))))        (a (b (c (d (e)))))
((mark sam) sam)                ((a b) b)
```
This prints the input in a 30-char column before printing the answer.
]