Skip to content

Commit c263302

Browse files
hyperpolymathclaude
andcommitted
feat(faces): wire resolve_face into lint_file + add RattleScript distribution layer
- resolve_face now wired into all six file handlers (parse, check, eval, compile, fmt, lint) — extension-based face inference complete - distributions/rattlescript/: thin Rust wrapper pre-injecting --face python, build.rs detects affinescript binary (env / PATH / monorepo sibling), justfile, examples (hello.rattle, ownership.rattle), README.adoc - RattleScript name is clear — no prior projects (verified) - Sync strategy: standalone repo uses affinescript as submodule; `just update-affinescript` bumps pointer on each release Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6390b81 commit c263302

8 files changed

Lines changed: 432 additions & 0 deletions

File tree

bin/main.ml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ let lex_file path =
5959
Format.eprintf "@[<v>%s:%d:%d: error: %s@]@." path pos.line pos.col msg;
6060
`Error (false, "Lexer error")
6161

62+
(** Resolve the effective face, promoting Canonical → face-from-extension when
63+
the file extension implies a specific face. This means a user can run
64+
[affinescript check hello.rattle] with no [--face] flag and get
65+
Python-face automatically. An explicit [--face canonical] overrides. *)
66+
let resolve_face face path =
67+
match face with
68+
| Affinescript.Face.Canonical ->
69+
let ext =
70+
try
71+
let dot = String.rindex path '.' in
72+
String.sub path (dot + 1) (String.length path - dot - 1)
73+
with Not_found -> ""
74+
in
75+
(match ext with
76+
| "rattle" -> Affinescript.Face.Python (* RattleScript *)
77+
| "pyaff" -> Affinescript.Face.Python
78+
| "jsaff" -> Affinescript.Face.Js
79+
| "pseudoaff" -> Affinescript.Face.Pseudocode
80+
| _ -> Affinescript.Face.Canonical)
81+
| other -> other (* explicit --face flag always wins *)
82+
6283
(** Parse a file using the requested face. *)
6384
let parse_with_face (face : Affinescript.Face.face) path =
6485
match face with
@@ -93,6 +114,7 @@ let preview_pseudocode_transform path =
93114

94115
(** Parse a file and print AST (no --json support). *)
95116
let parse_file (face : Affinescript.Face.face) path =
117+
let face = resolve_face face path in
96118
try
97119
let prog = parse_with_face face path in
98120
Format.printf "%s@." (Affinescript.Ast.show_program prog);
@@ -109,6 +131,7 @@ let parse_file (face : Affinescript.Face.face) path =
109131
(** Type-check a file. With [--json], emits a structured diagnostic
110132
report on stderr. *)
111133
let check_file face json path =
134+
let face = resolve_face face path in
112135
if json then begin
113136
let diags = ref [] in
114137
let add d = diags := d :: !diags in
@@ -182,6 +205,7 @@ let check_file face json path =
182205
(** Evaluate a file with the interpreter. With [--json], emits
183206
diagnostics on stderr instead of human-readable error text. *)
184207
let eval_file face json path =
208+
let face = resolve_face face path in
185209
if json then begin
186210
let diags = ref [] in
187211
let add d = diags := d :: !diags in
@@ -264,6 +288,7 @@ let repl_cmd_fn () =
264288
compilation errors. With [--wasm-gc], targets the WebAssembly GC
265289
proposal instead of WASM 1.0 linear memory. *)
266290
let compile_file face json wasm_gc path output =
291+
let face = resolve_face face path in
267292
if json then begin
268293
let diags = ref [] in
269294
let add d = diags := d :: !diags in
@@ -380,6 +405,7 @@ let compile_file face json wasm_gc path output =
380405
(** Format a file. Only canonical face is supported — non-canonical face
381406
formatting requires a reverse transform that is not yet implemented. *)
382407
let fmt_file face path =
408+
let face = resolve_face face path in
383409
(match face with
384410
| Affinescript.Face.Python ->
385411
Format.eprintf "fmt --face python is not yet supported \
@@ -407,6 +433,7 @@ let fmt_file face path =
407433
(** Lint a file. With [--json], emits lint diagnostics as structured
408434
JSON on stderr. *)
409435
let lint_file face json path =
436+
let face = resolve_face face path in
410437
if json then begin
411438
let diags = ref [] in
412439
let add d = diags := d :: !diags in
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
4+
[package]
5+
name = "rattle"
6+
version = "0.1.0"
7+
edition = "2021"
8+
description = "Python-syntax AffineScript — write Python-style code, get affine resource guarantees"
9+
license = "PMPL-1.0-or-later"
10+
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]
11+
repository = "https://github.com/hyperpolymath/rattlescript"
12+
keywords = ["affinescript", "python-face", "wasm", "affine-types", "linear-types"]
13+
categories = ["development-tools", "compilers"]
14+
15+
[dependencies]
16+
# Pure Rust thin wrapper — no external dependencies.
17+
18+
[build-dependencies]
19+
# build.rs detects or requires AFFINESCRIPT_BIN at compile time
20+
21+
[[bin]]
22+
name = "rattle"
23+
path = "src/main.rs"
24+
25+
[profile.release]
26+
opt-level = "z"
27+
lto = true
28+
strip = true
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
= RattleScript
4+
:toc:
5+
:toc-placement: preamble
6+
7+
Python-syntax AffineScript.
8+
Write code that looks like Python.
9+
Get affine resource guarantees, algebraic effects, and typed WASM out.
10+
11+
---
12+
13+
== What it is
14+
15+
RattleScript is AffineScript with its Python face pre-selected.
16+
If you write Python, you already know most of the syntax.
17+
The compiler checks that your resources (files, sockets, tokens) are used
18+
*exactly as many times as you say* — and proves it at compile time.
19+
20+
[source,python]
21+
----
22+
def greet(name: String) -> String:
23+
"Hello, " + name + "!"
24+
25+
def main() -> ():
26+
let msg = greet("world")
27+
IO.println(msg)
28+
----
29+
30+
That's a valid `.rattle` file. Run it:
31+
32+
[source,bash]
33+
----
34+
rattle eval hello.rattle
35+
----
36+
37+
== Why Python syntax?
38+
39+
Because Python programmers deserve a sound type system.
40+
RattleScript is Python's more dangerous cousin — same comfortable whitespace-
41+
delimited blocks, but with teeth: no null pointer exceptions, no
42+
use-after-free, no data races, and no runtime surprises that the type
43+
checker already knows about.
44+
45+
The mascot is a rattlesnake. Python's more dangerous cousin.
46+
47+
== Surface mappings
48+
49+
[cols="1,1"]
50+
|===
51+
|Python surface |AffineScript equivalent
52+
53+
|`def f(x: T) -> R:` |`fn f(x: T) -> R { ... }`
54+
|`True` / `False` |`true` / `false`
55+
|`None` |`()`
56+
|`and` / `or` / `not` |`&&` / `\|\|` / `!`
57+
|`class Name:` |`type Name`
58+
|`pass` |`()`
59+
|`import a.b` |`use a::b;`
60+
|`from a import b` |`use a::b;`
61+
|`if cond:` / `else:` |`if cond { ... } else { ... }`
62+
|`for x in e:` |`for x in e { ... }`
63+
|`match e:` |`match e { ... }`
64+
|`# comment` |`// comment`
65+
|===
66+
67+
See `rattle preview-python-transform <file>` to inspect the full transform
68+
for any file.
69+
70+
== Installation
71+
72+
=== From source (monorepo dev)
73+
74+
[source,bash]
75+
----
76+
# From nextgen-languages/affinescript root — build affinescript first:
77+
dune build
78+
79+
# Then build the rattle wrapper:
80+
cd distributions/rattlescript
81+
just build
82+
# Binary at: target/debug/rattle
83+
----
84+
85+
=== Installed release
86+
87+
[source,bash]
88+
----
89+
just install
90+
# Installs rattle to ~/.cargo/bin
91+
----
92+
93+
=== Standalone GitHub repo
94+
95+
The standalone https://github.com/hyperpolymath/rattlescript[hyperpolymath/rattlescript]
96+
repo contains a pre-configured distribution with affinescript as a git
97+
submodule. Use that for a self-contained checkout.
98+
99+
== Usage
100+
101+
[source,bash]
102+
----
103+
# Type-check a file
104+
rattle check hello.rattle
105+
106+
# Evaluate
107+
rattle eval hello.rattle
108+
109+
# Compile to WASM
110+
rattle compile hello.rattle
111+
112+
# See the canonical AffineScript generated by the preprocessor
113+
rattle preview-python-transform hello.rattle
114+
115+
# All affinescript subcommands work — --face python is pre-injected
116+
rattle lint hello.rattle
117+
rattle fmt hello.rattle
118+
----
119+
120+
== Relationship to AffineScript
121+
122+
RattleScript *is* AffineScript — same compiler, same type system, same WASM
123+
output. The only difference is `--face python` is the default.
124+
125+
The Python face lives in
126+
link:../../lib/python_face.ml[`lib/python_face.ml`] and the error vocabulary
127+
in link:../../lib/face.ml[`lib/face.ml`]. No compiler internals change when
128+
the face changes.
129+
130+
== Sync strategy
131+
132+
This directory (`distributions/rattlescript/`) lives inside the affinescript
133+
monorepo. The standalone https://github.com/hyperpolymath/rattlescript[rattlescript]
134+
GitHub repo uses affinescript as a git submodule and contains only:
135+
136+
* This `distributions/rattlescript/` content (copied at release time)
137+
* Branding assets
138+
* CI workflows
139+
140+
`just update-affinescript` advances the submodule pointer after each
141+
affinescript release. All language changes, face changes, and bug fixes
142+
come through that single submodule bump — no duplication.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
4+
//! build.rs — detect or require the `affinescript` binary path.
5+
//!
6+
//! Checks, in order:
7+
//! 1. `AFFINESCRIPT` env var (explicit override)
8+
//! 2. `affinescript` on PATH (standard install)
9+
//! 3. Sibling build output `../../_build/default/bin/main.exe` (monorepo dev)
10+
//!
11+
//! The resolved path is baked in as the `AFFINESCRIPT_BIN` compile-time env.
12+
13+
use std::env;
14+
use std::path::Path;
15+
16+
fn main() {
17+
println!("cargo:rerun-if-env-changed=AFFINESCRIPT");
18+
19+
let bin = if let Ok(p) = env::var("AFFINESCRIPT") {
20+
p
21+
} else if which_affinescript() {
22+
"affinescript".to_string()
23+
} else {
24+
// Monorepo dev path: distributions/rattlescript/ → ../../_build/...
25+
let manifest = env::var("CARGO_MANIFEST_DIR").unwrap();
26+
let dev_path = Path::new(&manifest)
27+
.join("../../_build/default/bin/main.exe")
28+
.canonicalize()
29+
.ok()
30+
.map(|p| p.to_string_lossy().into_owned());
31+
32+
match dev_path {
33+
Some(p) => p,
34+
None => {
35+
// Fall back to the name — will fail at runtime with a clear message.
36+
"affinescript".to_string()
37+
}
38+
}
39+
};
40+
41+
println!("cargo:rustc-env=AFFINESCRIPT_BIN={bin}");
42+
}
43+
44+
fn which_affinescript() -> bool {
45+
std::process::Command::new("which")
46+
.arg("affinescript")
47+
.output()
48+
.map(|o| o.status.success())
49+
.unwrap_or(false)
50+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
4+
# RattleScript — Hello World
5+
# This is a .rattle file (Python-face AffineScript).
6+
# You can also use .pyaff if you prefer.
7+
8+
def greet(name: String) -> String:
9+
"Hello, " + name + "!"
10+
11+
def main() -> ():
12+
let msg = greet("world")
13+
IO.println(msg)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
4+
# RattleScript — Ownership demo
5+
# Python programmers: this looks familiar, but the compiler tracks
6+
# *who owns what* so you never get null-pointer surprises.
7+
8+
def consume_token(@linear token: Token) -> Receipt:
9+
# token is used exactly once here — the compiler proves it
10+
process(token)
11+
12+
def safe_divide(a: Int, b: Int) -> Option[Int]:
13+
if b == 0:
14+
None
15+
else:
16+
Some(a / b)
17+
18+
def show_result(result: Option[Int]) -> ():
19+
match result:
20+
Some(n) => IO.println("Result: " + Int.to_string(n))
21+
None => IO.println("Division by zero — handled, not crashed")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath)
3+
4+
# RattleScript distribution justfile.
5+
# Most work lives in the parent affinescript justfile.
6+
# This file handles the thin Rust wrapper binary only.
7+
8+
default:
9+
just --list
10+
11+
# Build the rattle binary (dev mode, uses sibling _build/)
12+
build:
13+
cargo build
14+
15+
# Build optimised release binary
16+
release:
17+
cargo build --release
18+
19+
# Run the example
20+
run-hello:
21+
cargo run -- eval examples/hello.rattle
22+
23+
# Check example typechecks
24+
check-hello:
25+
cargo run -- check examples/hello.rattle
26+
27+
# Show the Python-face transform for the hello example
28+
preview-hello:
29+
cargo run -- preview-python-transform examples/hello.rattle
30+
31+
# Run all examples
32+
check-examples:
33+
cargo run -- check examples/hello.rattle
34+
cargo run -- check examples/ownership.rattle
35+
36+
# Install rattle binary to ~/.cargo/bin
37+
install:
38+
cargo install --path .
39+
40+
# Bump AffineScript submodule pointer (called by CI after affinescript releases)
41+
update-affinescript:
42+
#!/usr/bin/env bash
43+
set -euo pipefail
44+
git -C ../.. submodule update --remote
45+
git add ../..
46+
git commit -m "chore(affinescript): advance pointer to latest release"

0 commit comments

Comments
 (0)