Metadata-Version: 2.4
Name: like-a
Version: 0.1.1
Summary: Python in three syntax skins: indentation, braces, S-expressions
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# as

**One Python, four syntaxes.** `as` converts Python source between the
indentation skin you know and three it can wear: C-style braces, Lisp-style
S-expressions, and a Rust-flavored vocabulary over the brace skin. Same
semantics, different look — hence the name: `as lisp`, `as brace`, `as rust`,
`as py`.

```
def quicksort(items):                  def quicksort(items) {
    if len(items) <= 1:                    if len(items) <= 1 {
        return items                           return items;
    pivot, *rest = items;                }
    lo = [x for x in rest                pivot, *rest = items;
         if x < pivot]                   lo = [x for x in rest if x < pivot];
    hi = [x for x in rest                hi = [x for x in rest if x >= pivot];
         if x >= pivot]                  return quicksort(lo) + [pivot] + quicksort(hi);
    return quicksort(lo) + [pivot]    }
        + quicksort(hi)
```

```lisp
(defn quicksort (items)
  (if (<= (len items) 1) (return items))
  (set! '(pivot (* rest)) items)
  (set! lo (list-comp x (for [x in rest]) (when (< x pivot))))
  (set! hi (list-comp x (for [x in rest]) (when (>= x pivot))))
  (return (+ (+ (quicksort lo) [pivot]) (quicksort hi))))
```

```rust
fn quicksort(items) {
    if len(items) <= 1 {
        return items;
    }
    let pivot, *rest = items;
    let lo = [x for x in rest if x < pivot];
    let hi = [x for x in rest if x >= pivot];
    return quicksort(lo) + [pivot] + quicksort(hi);
}
```

## Why

"Braces or indentation" is one of the oldest holy wars in programming.
`as` does not pick a side — it makes the question meaningless. Write in
whichever skin you like, read in whichever skin you like, and argue about
taste instead of syntax. The round trip is *exact*: converting `py ->
skin -> py` yields an AST identical to the original (tested on the whole
`as` codebase and 40 stdlib modules, see Tests).

## Install & use

```
pip install like-a
```

```
as lisp foo.py            # Python -> S-expressions (stdout)
as brace foo.py -o foo.cpy
as rust foo.py -o foo.rrs
as py foo.lpy             # S-expressions -> Python
cat foo.py | as brace - --from py
```

Input style is detected from the extension: `.py`, `.cpy` (braces), `.lpy`
(lisp), `.rrs` (rust). Use `--from` to override.

The importable package is `aslang` (`from aslang import convert`) because
`as` is a Python keyword. The CLI keeps the two-letter name (`as lisp`,
`as brace`) — same phrase as `like a lisp`; the distribution on PyPI is
`like-a`, since PyPI forbids Python keywords as project names.

## The four skins

| skin   | extension | blocks             | collections                          |
|--------|-----------|--------------------|--------------------------------------|
| py     | `.py`     | indentation        | `()`, `[]`, `{}`, `{k: v}`           |
| brace  | `.cpy`    | `if cond { ... }`  | unchanged (Python expressions)       |
| rust   | `.rrs`    | `if cond { ... }`  | unchanged; `fn`, `let`, `else if`    |
| lisp   | `.lpy`    | `(if cond ...)`    | `[...]` list, `'(...)` tuple, `{(k v)}` dict, `#{...}` set |

The lisp skin is more than a re-bracketing: operators go prefix
(`(+ a (* b c))`), methods become calls (`(len items)`, `(items.append x)`),
comprehensions get `for`/`when` clauses, `match` cases carry their patterns
as data. The brace skin stays deliberately close to Python — only the block
structure changes. The rust skin is the brace skin plus surface vocabulary:
`def` prints as `fn`, plain assignments gain `let`, `elif` becomes
`else if`. Cosmetic Rust, not Rust — types stay Python's, `print` stays
`print`, `class` and `async` pass through with Python keywords intact.

### lpy form reference (selected)

```
(defn name (params...) [-> ret] body...)     (set! target value)
(for [x in iter] body... [(else ...)])       (while cond body...)
(if test then... [(elif ...)...] [(else ...)])
(with [(expr as name) ...] body...)
(try body... (except Type [as e] h...)...)
(match subject (case pattern [(when guard)] body...))
patterns: literal | name | _ | (as pat name) | (or p...) | [p *rest]
          {(key pat) ... (** rest)} | (Cls p... (kws (attr pat)...))
(list-comp elt (for [x in xs]) (when cond))
(fstr "x = " (fv value) "!")
```

## Correctness

- **py <-> brace** and **py <-> rust** convert text directly on the token
  stream: comments, formatting and even docstrings survive byte-for-byte
  outside the changed block lines (rust adds only the `fn`/`let`/`else if`
  vocabulary on top).
- **anything <-> lisp** goes through the `ast` module: the reader builds a
  real AST, the printer walks it. Comments are preserved best-effort
  (dropped by the `ast`, re-attached by line numbers).
- Round-trip proof: `ast.dump(ast.parse(src)) == ast.dump(parse(convert(
  convert(src, py, skin), skin, py)))` for every corpus file.

## Compared

|                       | bython | Hy        | Nim      | **as**           |
|-----------------------|--------|-----------|----------|------------------|
| direction             | -> py  | -> py AST | own lang  | **any -> any**    |
| braces skin           | ✓      | —         | ✓        | ✓                |
| S-expression skin     | —      | ✓ (own)   | —        | ✓                |
| round-trip guaranteed | —      | —         | —        | ✓ (AST-equal)    |
| Python semantics      | ✓      | mostly    | no       | ✓ exactly        |

## Limitations

- `match` is supported in all skins; `typing.TypeAlias` statements and
  `try*`-era syntax are not yet in the lisp skin.
- The lisp reader accepts a fixed set of special forms; arbitrary macro
  machinery is out of scope (this is a converter, not a language).
- A case body that begins with a call to a function literally named `when`
  needs parentheses discipline (the `(when ...)` guard marker wins).

## Possible next

The rust skin (0.1.1) started as a joke feature request in the article
comments; the C indentation skin is the serious one, and it is
deliberately out of scope for now: in C, braces are load-bearing
(`enum {A, B}`, aggregate initializers, GNU `({ ... })` statements),
so separating block braces from literal braces needs a real parser
(libclang/tree-sitter), not token rewriting - and the round trip
would be AST-level, not byte-level. Demand cuts both ways: some like
everything about Nim except its Python-style indentation, and Nim has
no brace form (its braces are taken by set/table literals and pragmas)
- so skins could run in both directions. No slight to Nim:
braces-for-data is a design choice, the canonical indentation form
stays canonical - skins would add contributors, not churn.
Interest-gated: if people ask for it, it may happen.

## Note on the name

Yes, `as` shadows GNU `as` (the assembler) if `~/.local/bin` precedes
`/usr/bin` in your `PATH`. **gcc invokes `as` via PATH — shell aliases
don't help.** If you build C/C++, either keep `/usr/bin` ahead of
`~/.local/bin` for build shells, pass `-B /usr/bin` to gcc, or
`pip uninstall like-a` (C beats curly braces sometimes). The two-letter
namespace was simply too good to leave unused — `import json as j`,
`as lisp foo.py`.

## Development

```
pip install -e .
pytest                  # 138 tests: unit + stdlib corpus + self-application
```

The tool digests its own source in the test suite: every module of `aslang`
is converted to every skin, back, and the result must be AST-identical —
and must still execute.

MIT license.
