Skip to content

Commit 8691fc6

Browse files
hyperpolymathclaude
andcommitted
compiler: fix AffineScript module system to unblock the per-module port
The .res -> .affine port needs sibling compiler modules to import the shared AST (Location/Token/Position/...) from Types. AffineScript's module resolver exported imported enum constructors but dropped imported struct/alias type definitions, so cross-module struct field access failed ("Field not found"). Per the chosen approach (fix the resolver, not single-file/accessors): - patches/affinescript-module-struct-fields.patch: threads imported modules' type_env + constructor_env into the importing module's typecheck context (typecheck.ml check_program gains ?import_type_env/?import_constructor_env; resolve.ml import_type_defs copies them across all three import forms; bin/main.ml passes them at the check/compile/eval entry points). Documented in patches/README.adoc; pending upstream to hyperpolymath/affinescript. - compiler/src/Types.affine: now a proper `module Types;` with `pub` exports. - verification/check-affinescript.sh: checks from compiler/src so `use Types::{...}` resolves via the loader's current-dir search. - scripts/install-affinescript-toolchain.sh: applies the patch after cloning, before building (idempotent). Verified: a module importing Types' structs with nested field access, struct construction, and enum-field matching type-checks; affinescript's own stdlib cross-module imports (http_fetch/option/io) still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM
1 parent 27571a3 commit 8691fc6

5 files changed

Lines changed: 232 additions & 26 deletions

File tree

compiler/src/Types.affine

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,28 @@
55
// * array<T> -> [T]; option<T> -> Option<T>; dict<k,v> -> Dict<k,v>; tuples kept.
66
// * Token variants `Float`/`String` renamed `FloatTok`/`StringTok`
77
// (`Float`/`String` are reserved type keywords in AffineScript).
8+
//
9+
// This is the `Types` module; sibling compiler modules `use Types::{...}`.
10+
// Cross-module struct field access requires the affinescript module-resolver
11+
// fix in patches/affinescript-module-struct-fields.patch.
12+
13+
module Types;
814

915
use prelude::*;
1016

11-
struct Position {
17+
pub struct Position {
1218
line: Int,
1319
column: Int,
1420
offset: Int
1521
}
1622

17-
struct Location {
23+
pub struct Location {
1824
start: Position,
1925
end_: Position,
2026
file: String
2127
}
2228

23-
enum TokenType {
29+
pub enum TokenType {
2430
// Keywords
2531
Main, End, Let, Mutable, Function, Struct, If, Elseif, Else, While, For, In,
2632
Break, Continue, Return, And, Or, Not, True, False, Nil, Gutter, Fn,
@@ -41,7 +47,7 @@ enum TokenType {
4147
Newline, EOF, Error(String)
4248
}
4349

44-
struct Token {
50+
pub struct Token {
4551
type_: TokenType,
4652
lexeme: String,
4753
loc: Location
@@ -51,7 +57,7 @@ struct Token {
5157
// AST
5258
// ============================================
5359

54-
enum Expr {
60+
pub enum Expr {
5561
IntLit(Int, Location),
5662
FloatLit(Float, Location),
5763
StringLit(String, Location),
@@ -68,22 +74,22 @@ enum Expr {
6874
Lambda([Param], Option<TypeExpr>, LambdaBody, Location)
6975
}
7076

71-
enum BinaryOp {
77+
pub enum BinaryOp {
7278
Add, Sub, Mul, Div, Mod,
7379
Eq, Neq, Lt, Gt, Lte, Gte,
7480
BAnd, BOr, BXor, Shl, Shr,
7581
LAnd, LOr
7682
}
7783

78-
enum UnaryOp { Neg, LNot, BNot }
84+
pub enum UnaryOp { Neg, LNot, BNot }
7985

80-
struct Param {
86+
pub struct Param {
8187
name: String,
8288
type_: Option<TypeExpr>,
8389
loc: Location
8490
}
8591

86-
enum TypeExpr {
92+
pub enum TypeExpr {
8793
TyInt,
8894
TyFloat,
8995
TyString,
@@ -97,12 +103,12 @@ enum TypeExpr {
97103
TyIdent(String)
98104
}
99105

100-
enum LambdaBody {
106+
pub enum LambdaBody {
101107
LambdaExpr(Expr),
102108
LambdaBlock([Stmt])
103109
}
104110

105-
enum Stmt {
111+
pub enum Stmt {
106112
// inline records -> positional: (mutable_, name, type_, value, loc)
107113
LetStmt(Bool, String, Option<TypeExpr>, Expr, Location),
108114
// (target, value, loc)
@@ -124,7 +130,7 @@ enum Stmt {
124130
ExprStmt(Expr)
125131
}
126132

127-
enum Decl {
133+
pub enum Decl {
128134
// (name, params, returnType, body, loc)
129135
FunctionDecl(String, [Param], Option<TypeExpr>, [Stmt], Location),
130136
// (name, fields, loc)
@@ -134,7 +140,7 @@ enum Decl {
134140
StmtDecl(Stmt)
135141
}
136142

137-
struct Program {
143+
pub struct Program {
138144
declarations: [Decl],
139145
loc: Location
140146
}
@@ -143,11 +149,11 @@ struct Program {
143149
// Errors
144150
// ============================================
145151

146-
enum ErrorCode {
152+
pub enum ErrorCode {
147153
E0001, E0002, E0003, E0004, E0005, E0006, E0007, E0008, E0009, E0010
148154
}
149155

150-
struct Diagnostic {
156+
pub struct Diagnostic {
151157
code: ErrorCode,
152158
message: String,
153159
loc: Location,
@@ -159,7 +165,7 @@ struct Diagnostic {
159165
// Runtime state & stability
160166
// ============================================
161167

162-
enum StabilityFactor {
168+
pub enum StabilityFactor {
163169
MutableState(Int, Int), // mutations, readers
164170
TypeInstability(Int), // reassignments
165171
NullPropagation(Int), // depth
@@ -170,14 +176,14 @@ enum StabilityFactor {
170176
RaceCondition(Int) // conflicts
171177
}
172178

173-
struct StabilityReport {
179+
pub struct StabilityReport {
174180
score: Int,
175181
factors: [StabilityFactor],
176182
breakdown: Dict<String, Int>,
177183
recommendations: [String]
178184
}
179185

180-
struct RuntimeState {
186+
pub struct RuntimeState {
181187
runCounter: Int,
182188
stabilityScore: Int,
183189
lastError: Option<ErrorCode>,
@@ -187,7 +193,7 @@ struct RuntimeState {
187193
historicalRuns: [Int]
188194
}
189195

190-
fn make_default_state() -> RuntimeState {
196+
pub fn make_default_state() -> RuntimeState {
191197
#{
192198
runCounter: 0,
193199
stabilityScore: 100,
@@ -200,7 +206,7 @@ fn make_default_state() -> RuntimeState {
200206
}
201207

202208
// Stability impact (non-positive), mirrors Types.res `stabilityImpact`.
203-
fn stability_impact(factor: StabilityFactor) -> Int {
209+
pub fn stability_impact(factor: StabilityFactor) -> Int {
204210
match factor {
205211
MutableState(mutations, readers) => -(10 * mutations + 5 * readers),
206212
TypeInstability(reassignments) => -(15 * reassignments),
@@ -214,12 +220,12 @@ fn stability_impact(factor: StabilityFactor) -> Int {
214220
}
215221

216222
// mirrors Types.res `calculateStability`: max(0, 100 + sum(impacts))
217-
fn calculate_stability(factors: [StabilityFactor]) -> Int {
223+
pub fn calculate_stability(factors: [StabilityFactor]) -> Int {
218224
let penalties = fold(factors, 0, |acc, x| acc + stability_impact(x));
219225
max(0, 100 + penalties)
220226
}
221227

222-
fn error_code_to_string(code: ErrorCode) -> String {
228+
pub fn error_code_to_string(code: ErrorCode) -> String {
223229
match code {
224230
E0001 => "E0001",
225231
E0002 => "E0002",

patches/README.adoc

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= AffineScript toolchain patches
4+
:toc:
5+
6+
Patches applied to a fresh `hyperpolymath/affinescript` checkout by
7+
`scripts/install-affinescript-toolchain.sh` before building the compiler, to
8+
support the ReScript -> AffineScript migration. Each is a stop-gap pending
9+
upstream; drop it from the install script once upstreamed.
10+
11+
== affinescript-module-struct-fields.patch
12+
13+
*Problem.* AffineScript's module resolver exported imported *enum* constructors
14+
but dropped imported *struct/alias* type definitions: `lib/resolve.ml` registered
15+
`TyEnum` variant constructors for imported modules but had a bare
16+
`| TyAlias _ | TyStruct _ | TyExtern -> ()`. Field access on an imported struct
17+
therefore failed with `Field 'x' not found in type T` — the named type stayed an
18+
opaque `TCon` and never expanded to its `TRecord`.
19+
20+
This blocked a per-module `.res -> .affine` port: the shared AST in
21+
`compiler/src/Types.affine` (`Location`, `Token`, `Position`, ...) is
22+
field-accessed by every other compiler module.
23+
24+
*Fix.* Thread imported modules' `type_env` (struct/alias/enum type definitions)
25+
and `constructor_env` into the importing module's type-check context, mirroring
26+
the existing `name_types` (scheme) threading that already made enums cross:
27+
28+
* `lib/typecheck.ml` — `check_program` gains `?import_type_env` /
29+
`?import_constructor_env`, seeding `ctx.type_env` / `ctx.constructor_env`.
30+
* `lib/resolve.ml` — a new `import_type_defs` copies a resolved module's
31+
`type_env` / `constructor_env` into the destination context, called from all
32+
three import forms (`use M;`, `use M::{...}`, `use M::*`); the imported-module
33+
check site passes the new params.
34+
* `bin/main.ml` — the `check` / `compile` / `eval` entry points pass the threaded
35+
`type_env` / `constructor_env` to `check_program`.
36+
37+
*Verification.* A module importing a struct and accessing its (nested) fields,
38+
constructing it, and matching on an imported enum field now type-checks; existing
39+
stdlib cross-module imports (`http_fetch`, `option`, `io`) still pass.
40+
41+
*Upstream.* This belongs in `hyperpolymath/affinescript` — it fixes any multi-file
42+
AffineScript program, not only error-lang. It could not be pushed from the
43+
migration session (affinescript was outside the session's repo scope); upstream
44+
when convenient, then delete this patch and its hook in the install script.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
diff --git a/bin/main.ml b/bin/main.ml
2+
index 8230ab2..b772a3c 100644
3+
--- a/bin/main.ml
4+
+++ b/bin/main.ml
5+
@@ -195,6 +195,8 @@ let check_file face json path =
6+
resolve_refs := List.rev resolve_ctx.references;
7+
(match Affinescript.Typecheck.check_program
8+
~import_types:type_ctx.Affinescript.Typecheck.name_types
9+
+ ~import_type_env:type_ctx.Affinescript.Typecheck.type_env
10+
+ ~import_constructor_env:type_ctx.Affinescript.Typecheck.constructor_env
11+
resolve_ctx.symbols prog with
12+
| Error e ->
13+
add (Affinescript.Json_output.of_type_error e)
14+
@@ -242,6 +244,8 @@ let check_file face json path =
15+
| Ok (resolve_ctx, type_ctx) ->
16+
(match Affinescript.Typecheck.check_program
17+
~import_types:type_ctx.Affinescript.Typecheck.name_types
18+
+ ~import_type_env:type_ctx.Affinescript.Typecheck.type_env
19+
+ ~import_constructor_env:type_ctx.Affinescript.Typecheck.constructor_env
20+
resolve_ctx.symbols prog with
21+
| Error e ->
22+
Format.eprintf "@[<v>%s@]@."
23+
@@ -505,6 +509,8 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
24+
| Ok (resolve_ctx, import_type_ctx) ->
25+
(match Affinescript.Typecheck.check_program
26+
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
27+
+ ~import_type_env:import_type_ctx.Affinescript.Typecheck.type_env
28+
+ ~import_constructor_env:import_type_ctx.Affinescript.Typecheck.constructor_env
29+
resolve_ctx.symbols prog with
30+
| Error e ->
31+
add (Affinescript.Json_output.of_type_error e)
32+
@@ -732,6 +738,8 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
33+
| Ok (resolve_ctx, import_type_ctx) ->
34+
(match Affinescript.Typecheck.check_program
35+
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
36+
+ ~import_type_env:import_type_ctx.Affinescript.Typecheck.type_env
37+
+ ~import_constructor_env:import_type_ctx.Affinescript.Typecheck.constructor_env
38+
resolve_ctx.symbols prog with
39+
| Error e ->
40+
Format.eprintf "@[<v>%s@]@."
41+
diff --git a/lib/resolve.ml b/lib/resolve.ml
42+
index 65a6b48..65a9206 100644
43+
--- a/lib/resolve.ml
44+
+++ b/lib/resolve.ml
45+
@@ -654,6 +654,20 @@ let import_specific_items
46+
Error (UndefinedVariable item.ii_name, item.ii_name.span)
47+
) (Ok ()) items
48+
49+
+(** Thread imported struct/alias/enum type definitions and value constructors
50+
+ from a resolved source module into the destination type-check context, so
51+
+ that field access on an imported struct resolves (companion to the scheme
52+
+ imports above — a struct needs its TRecord definition, not just a
53+
+ name_types scheme). *)
54+
+let import_type_defs
55+
+ (dest : Typecheck.context) (source : Typecheck.context) : unit =
56+
+ Hashtbl.iter (fun name ty ->
57+
+ Hashtbl.replace dest.Typecheck.type_env name ty
58+
+ ) source.Typecheck.type_env;
59+
+ Hashtbl.iter (fun name ty ->
60+
+ Hashtbl.replace dest.Typecheck.constructor_env name ty
61+
+ ) source.Typecheck.constructor_env
62+
+
63+
(** Resolve imports in a program using module loader *)
64+
let rec resolve_and_typecheck_module
65+
(loader : Module_loader.t)
66+
@@ -698,7 +712,10 @@ let rec resolve_and_typecheck_module
67+
imported modules must check the same way top-level programs do.) *)
68+
match
69+
Typecheck.check_program
70+
- ~import_types:type_ctx.Typecheck.name_types symbols prog
71+
+ ~import_types:type_ctx.Typecheck.name_types
72+
+ ~import_type_env:type_ctx.Typecheck.type_env
73+
+ ~import_constructor_env:type_ctx.Typecheck.constructor_env
74+
+ symbols prog
75+
with
76+
| Ok final_ctx -> Ok (symbols, final_ctx)
77+
| Error type_err ->
78+
@@ -729,6 +746,7 @@ and resolve_imports_with_loader
79+
mod_type_ctx.Typecheck.var_types
80+
mod_type_ctx.Typecheck.name_types
81+
alias_str;
82+
+ import_type_defs type_ctx mod_type_ctx;
83+
Ok ()
84+
| Error e -> Error e
85+
end
86+
@@ -747,13 +765,15 @@ and resolve_imports_with_loader
87+
(* Resolve and type-check the module *)
88+
begin match resolve_and_typecheck_module loader loaded_mod with
89+
| Ok (mod_symbols, mod_type_ctx) ->
90+
- import_specific_items ctx.symbols
91+
+ let* () = import_specific_items ctx.symbols
92+
type_ctx.Typecheck.var_types
93+
type_ctx.Typecheck.name_types
94+
mod_symbols
95+
mod_type_ctx.Typecheck.var_types
96+
mod_type_ctx.Typecheck.name_types
97+
- items
98+
+ items in
99+
+ import_type_defs type_ctx mod_type_ctx;
100+
+ Ok ()
101+
| Error e -> Error e
102+
end
103+
| Error (Module_loader.ModuleNotFound _) ->
104+
@@ -785,6 +805,7 @@ and resolve_imports_with_loader
105+
sym)
106+
| _ -> ()
107+
) mod_symbols.all_symbols;
108+
+ import_type_defs type_ctx mod_type_ctx;
109+
Ok ()
110+
| Error e -> Error e
111+
end
112+
diff --git a/lib/typecheck.ml b/lib/typecheck.ml
113+
index e38e302..4390c71 100644
114+
--- a/lib/typecheck.ml
115+
+++ b/lib/typecheck.ml
116+
@@ -2401,6 +2401,8 @@ let populate_call_effects (ctx : context) (prog : Ast.program) : unit =
117+
Effect_sites.set_async_by_ord async_tbl
118+
119+
let check_program ?(import_types : (string, scheme) Hashtbl.t option)
120+
+ ?(import_type_env : (string, ty) Hashtbl.t option)
121+
+ ?(import_constructor_env : (string, ty) Hashtbl.t option)
122+
(symbols : Symbol.t) (prog : Ast.program)
123+
: (context, type_error) Result.t =
124+
try
125+
@@ -2423,6 +2425,17 @@ let check_program ?(import_types : (string, scheme) Hashtbl.t option)
126+
Option.iter (fun tbl ->
127+
Hashtbl.iter (fun name sc -> Hashtbl.replace ctx.name_types name sc) tbl
128+
) import_types;
129+
+ (* Thread imported struct/alias/enum type definitions (type_env) and value
130+
+ constructors (constructor_env) so field access on an imported struct
131+
+ resolves: enums already cross via name_types schemes, but a struct's
132+
+ TRecord definition must be present for a named param type to expand to
133+
+ its fields (otherwise it stays an opaque TCon -> FieldNotFound). *)
134+
+ Option.iter (fun tbl ->
135+
+ Hashtbl.iter (fun name ty -> Hashtbl.replace ctx.type_env name ty) tbl
136+
+ ) import_type_env;
137+
+ Option.iter (fun tbl ->
138+
+ Hashtbl.iter (fun name ty -> Hashtbl.replace ctx.constructor_env name ty) tbl
139+
+ ) import_constructor_env;
140+
(* Forward pass: register all types, effects, traits, impls, and
141+
function signatures so that mutually recursive declarations resolve. *)
142+
let* () = List.fold_left (fun acc decl ->

scripts/install-affinescript-toolchain.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,20 @@ $SUDO apt-get install -y \
2929
libfmt-ocaml-dev libcmdliner-ocaml-dev libyojson-ocaml-dev \
3030
libppxlib-ocaml-dev libjs-of-ocaml-dev
3131

32-
# 2. Fetch + build the compiler binary.
32+
# 2. Fetch the compiler.
3333
[ -d "$AFFINE_SRC/.git" ] || git clone --depth 1 "$AFFINE_REPO" "$AFFINE_SRC"
34+
35+
# 2a. Apply the module-resolver fix (export imported struct field definitions so
36+
# cross-module struct field access type-checks) until it is upstreamed to
37+
# affinescript. See patches/README.adoc. Idempotent: skipped if already applied.
38+
PATCH="$(cd "$(dirname "$0")/.." && pwd)/patches/affinescript-module-struct-fields.patch"
39+
if [ -f "$PATCH" ] && git -C "$AFFINE_SRC" apply --check "$PATCH" 2>/dev/null; then
40+
git -C "$AFFINE_SRC" apply "$PATCH" && echo "applied $PATCH"
41+
else
42+
echo "module-struct-fields patch: already applied or not applicable — continuing"
43+
fi
44+
45+
# 3. Build the compiler binary.
3446
( cd "$AFFINE_SRC" && dune build bin/main.exe )
3547

3648
# 3. Install the binary + stdlib. The module loader discovers the stdlib at

0 commit comments

Comments
 (0)