Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions lib/bash_codegen.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)

(** Bash/POSIX shell emitter (heavily restricted MVP).

Bash is genuinely limited: no first-class numbers, no closures, no
structures. We accept Int-only programs whose entry returns Int (used
as the shell exit code). Functions are emitted as Bash functions that
[echo] their result; callers read it via [$(fn args)]. *)

open Ast

exception Bash_unsupported of string
let unsupported m = raise (Bash_unsupported m)

let bash_reserved = [
"if"; "then"; "else"; "elif"; "fi"; "case"; "esac";
"for"; "while"; "until"; "do"; "done"; "function"; "return";
"in"; "break"; "continue"; "exit"; "true"; "false";
]

let mangle s = if List.mem s bash_reserved then s ^ "_" else s

(* Bash arithmetic only: every expression must evaluate to an integer. *)
let rec gen_expr (e : expr) : string =
match e with
| ExprLit (LitInt (n, _)) -> string_of_int n
| ExprLit (LitBool (true, _)) -> "1"
| ExprLit (LitBool (false, _))-> "0"
| ExprLit _ -> unsupported "Bash backend accepts only Int/Bool literals"
| ExprVar id -> "$" ^ mangle id.name
| ExprBinary (a, op, b) ->
let s = match op with
| OpAdd -> "+" | OpSub -> "-" | OpMul -> "*" | OpDiv -> "/" | OpMod -> "%"
| OpEq -> "==" | OpNe -> "!="
| OpLt -> "<" | OpLe -> "<=" | OpGt -> ">" | OpGe -> ">="
| OpAnd -> "&&" | OpOr -> "||"
| OpBitAnd -> "&" | OpBitOr -> "|" | OpBitXor -> "^"
| OpShl -> "<<" | OpShr -> ">>"
| OpConcat -> unsupported "string concat not supported in Bash backend"
in
"(" ^ gen_expr a ^ " " ^ s ^ " " ^ gen_expr b ^ ")"
| ExprUnary (OpNeg, x) -> "(-" ^ gen_expr x ^ ")"
| ExprUnary (OpNot, x) -> "(! " ^ gen_expr x ^ ")"
| ExprUnary _ -> unsupported "unary op not supported in Bash backend"
| ExprApp (callee, args) ->
let name = match callee with
| ExprVar id -> id.name
| _ -> unsupported "indirect call not supported in Bash backend"
in
let arg_strs = List.map gen_expr args in
(* Call a Bash function and capture its echo: $(fn 1 2) *)
Printf.sprintf "$(%s %s)" (mangle name) (String.concat " " arg_strs)
| ExprIf { ei_cond; ei_then; ei_else } ->
let c = gen_expr ei_cond in
let t = gen_expr ei_then in
let f = match ei_else with Some e -> gen_expr e | None -> "0" in
(* Bash arithmetic ternary: $((c ? t : f)) — works in all $((...)) *)
Printf.sprintf "(%s ? %s : %s)" c t f
| ExprLet { el_pat = PatVar _; _ } ->
unsupported "expression-position let not supported in Bash backend"
| ExprSpan (inner, _) -> gen_expr inner
| ExprBlock _ -> unsupported "block expressions not supported in Bash backend"
| _ -> unsupported "expression form not supported in Bash backend"

let gen_function (fd : fn_decl) : string =
let name = mangle fd.fd_name.name in
let params = fd.fd_params in
let body_expr = match fd.fd_body with
| FnExpr e -> e
| FnBlock { blk_stmts = []; blk_expr = Some e } -> e
| FnBlock _ ->
unsupported "Bash backend accepts only single-expression function bodies"
in
let body_str = gen_expr body_expr in
let buf = Buffer.create 128 in
Buffer.add_string buf (name ^ "() {\n");
List.iteri (fun i (p : param) ->
Buffer.add_string buf
(Printf.sprintf " local %s=$%d\n" (mangle p.p_name.name) (i + 1))
) params;
Buffer.add_string buf (Printf.sprintf " echo $((%s))\n}\n\n" body_str);
Buffer.contents buf

let generate (program : program) (_symbols : Symbol.t) : string =
let buf = Buffer.create 1024 in
Buffer.add_string buf "#!/usr/bin/env bash\n";
Buffer.add_string buf "# Generated by AffineScript compiler\n";
Buffer.add_string buf "# SPDX-License-Identifier: PMPL-1.0-or-later\n";
Buffer.add_string buf "set -euo pipefail\n\n";
List.iter (function
| TopFn fd -> Buffer.add_string buf (gen_function fd)
| _ -> ()
) program.prog_decls;
let has_main = List.exists (function
| TopFn fd -> fd.fd_name.name = "main"
| _ -> false) program.prog_decls in
if has_main then Buffer.add_string buf "exit $(main)\n";
Buffer.contents buf

let codegen_bash (program : program) (symbols : Symbol.t) : (string, string) result =
try Ok (generate program symbols)
with
| Bash_unsupported msg -> Error ("Bash backend: " ^ msg)
| Failure msg -> Error ("Bash codegen error: " ^ msg)
| e -> Error ("Bash codegen error: " ^ Printexc.to_string e)
Loading
Loading