Skip to content

Commit 5c3a0b3

Browse files
ReScript→AffineScript migration: Cst.affine (module 3) (#28)
* error-lang: replace fabricated ABI "proofs" with machine-checked ones The three "Safety Proofs" in src/abi/Foreign.idr (stabilityBounded, positionalDeterministic, paradoxMonotonic) were not proofs: each fabricated its evidence with cast ()/cast Refl over an IO action. Replace them with genuine, self-contained Idris2 modules, machine-checked under Idris 2 v0.8.0 (no believe_me / assert_total / cast / postulate): - Stability.idr : stability score is bounded in [0,100] (clamp model of compiler/src/Types.res calculateStability). - Positional.idr : positional-operator behaviour is deterministic over the pure model of the Zig FFI (a genuine Refl, not IO cast Refl). - Paradox.idr : the two threshold-gated factors are monotone; the blanket "paradox detection monotonic" claim is RETRACTED -- proving it honestly surfaced that scope_leakage is prime-gated and therefore non-monotone (line 7 prime, line 8 not). Foreign.idr is reduced to an honest, self-contained ABI binding layer. Add error-lang-abi.ipkg and verification/check-proofs.sh (idris2 --check all four modules). Rewrite PROOF-NEEDS.md to record what is proved, what is retracted, the toolchain, and open conformance obligations. The language's satirical "100% production-ready / formally verified" self-presentation in README/WHITEPAPER is intentional and left intact; this change only makes the underlying proof artifacts real and honest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * docs: design Error-Lang as a Trope IR front end (trope-particularity integration) Adds docs/Trope-Particularity-Integration.adoc — a design (not yet implemented) for lowering Error-Lang's Echo operations and stability factors to the language-neutral Trope IR (hyperpolymath/trope-checker, v0.1 prevent profile) and consuming the verified verdict + witness. - Object/effect/grade correspondence: Echo<A,B> <-> Trope[Phi], EchoR <-> FloatingQuality, echo/echo_to_residue/echo_input/echo_output <-> preserve/ detach/project. echo_to_residue IS detach (bond=Severed, irrecoverable), matching the [Stab-Erase] debit and "decomposition must be visible". - stabilityFactor -> grade mapping; the silent instabilities (GlobalState, RaceCondition) land on the deceptive Conflated bottom -> a lowering fault under the prevent profile. - Verdict mapping: scalar calculateStability -> use-model floor + p-sufficient/ p-insufficient + witness edge (the invariant-path argmin Stability.idr already reasons about). - Architecture (reference, never vendor; schema is the trust boundary), the per-front-end O2 lowering-correctness obligations (L-Echo/L-Grade/L-Silent/ L-Floor), and a 4-phase plan. Builds on docs/Echo-Decomposition.adoc; references echo-types, trope-checker and trope-particularity-workbench by URL only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * docs: qualify external repo paths to clear structural_drift (SD022) Hypatia structural_drift flagged `src/idris2/` and `src/aggregate/` in the trope-integration design as dangling references "surviving a directory rename". Both are deliberately *external*: the trope-checker repo's Idris2 core and the panic-attack repo's aggregate module. Reword as unambiguously external (drop the bare `src/<dir>/` form) so the heuristic no longer reads them as internal error-lang tree paths. No substantive content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: begin ReScript->AffineScript migration — port Types.res First module of the compiler's migration to AffineScript (the Hyperpolymath language policy bans ReScript). Adds compiler/src/Types.affine, a faithful port of compiler/src/Types.res, verified green with `affinescript check`: - all token / AST / error / stability types (structs + enums + match) - ReScript inline-record variants lowered to positional constructor args - token variants Float/String renamed FloatTok/StringTok (reserved type keywords in AffineScript) - Echo types (TyEcho / TyEchoResidue) shaped Trope-IR-ready per docs/Trope-Particularity-Integration.adoc (Phase 0) - make_default_state, stability_impact, calculate_stability, error_code_to_string Toolchain, so the .affine sources are reproducibly CI-verifiable: - scripts/install-affinescript-toolchain.sh — builds the AffineScript compiler from distro OCaml packages (independent of opam.ocaml.org) + installs the binary and stdlib under a discoverable share/ path - verification/check-affinescript.sh — typechecks all compiler/src/*.affine Types.res is retained until its dependents migrate; format_diagnostic is deferred pending the string / affine-borrow pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * 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 * compiler: port Lexer.res -> Lexer.affine Second compiler module. Lexer.affine imports Types (`use Types::*;`) and ports the tokenizer in AffineScript's functional style: with no mutable struct fields or record-spread, state-mutating helpers take a LexerState and return a new one, and the scanners + driver loop a `let mut` local. Single chars are Char (char_at / char_to_int); lexemes and the escape buffer use substring; numeric literals use the parse_int / parse_float builtins; the keyword Dict becomes a string-equality lookup. Verified green with `affinescript check`. Faithful for the core path (identifiers/keywords, decimal int + float + exponent, strings with escapes, all operators/delimiters, comments, newline + EOF, error recovery + diagnostics E0001-E0004). Documented parity gaps for a follow-up pass: hex/binary integer prefixes, triple-quoted strings + \0 escape, smart-quote E0007. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port Cst.res -> Cst.affine (concrete syntax tree + trivia) Third module. Trivia-preserving CST with source round-trip. Functional port: classify_trivia loops a `let mut` local; lex_with_trivia folds the raw tokens carrying the last token in hand, so newline-trailing-trivia and trailing source attach without array-index mutation. Selective `Types` import (Cst's node kinds collide with Types' Decl/Stmt constructors). Verified with `affinescript check`. Omitted, documented in-file: - node_at: returning a deepest *subtree* needs a node used both to recurse into AND to return — not expressible under affine ownership without a shared/clone type. Auxiliary (IDE cursor lookup); deferred. - run_tests: the Console-based test harness is not compiler code. Encountered an affinescript resolver bug along the way: a local `let len = ...` shadows the builtin `len` *globally* (so a later `len(xs)` typed as Int). Worked around by renaming the local; worth an upstream fix in affinescript. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2821914 commit 5c3a0b3

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

compiler/src/Cst.affine

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Cst.affine — Concrete Syntax Tree for Error-Lang (ported from compiler/src/Cst.res).
3+
//
4+
// A CST preserves all source text (whitespace, comments, exact tokens) so source
5+
// round-trips. Ported to AffineScript's functional style (no mut struct fields /
6+
// record spread): stateful classify_trivia loops a `let mut` local; lex_with_trivia
7+
// folds raw tokens carrying the last token in hand (so newline-trailing-trivia and
8+
// trailing source are attached without array-index mutation).
9+
//
10+
// Selective `Types` import: Cst's node kinds (MainBlock/FunctionDecl/LetStmt/...)
11+
// would collide with Types' Decl/Stmt constructors under a glob import, so only the
12+
// token/location types (+ the Newline constructor for matching) are imported.
13+
//
14+
// Omitted vs Cst.res (documented):
15+
// - node_at: returning a deepest *subtree* needs a node used both to recurse into
16+
// and to return — not expressible under affine ownership without a shared/clone
17+
// type. Auxiliary (IDE cursor lookup); deferred.
18+
// - run_tests: the test harness (Console-based) is not part of the compiler.
19+
20+
module Cst;
21+
22+
use prelude::*;
23+
use string::{length, substring, join, char_at};
24+
use collections::{flat_map};
25+
use Types::{TokenType, Position, Location, Token, Newline};
26+
use Lexer::{lex};
27+
28+
// ---- trivia ----
29+
30+
enum TriviaKind { TkWhitespace, TkLineComment, TkNewline }
31+
32+
struct Trivia {
33+
kind: TriviaKind,
34+
text: String,
35+
loc: Location
36+
}
37+
38+
struct CstToken {
39+
tokenKind: TokenType,
40+
text: String,
41+
leadingTrivia: [Trivia],
42+
trailingTrivia: [Trivia],
43+
loc: Location
44+
}
45+
46+
enum CstNodeKind {
47+
SourceFile, MainBlock, FunctionDecl, StructDecl, LetStmt, IfStmt, WhileStmt,
48+
ForStmt, ReturnStmt, BreakStmt, ContinueStmt, PrintStmt, GutterBlock, ExprStmt,
49+
BinaryExpr, UnaryExpr, CallExpr, IndexExpr, MemberExpr, TernaryExpr, LambdaExpr,
50+
ArrayLitExpr, ParamList, ArgList, ErrorNode
51+
}
52+
53+
enum CstNode {
54+
NodeToken(CstToken),
55+
NodeTree(CstTree)
56+
}
57+
58+
struct CstTree {
59+
kind: CstNodeKind,
60+
children: [CstNode],
61+
loc: Location
62+
}
63+
64+
// ---- location helpers ----
65+
66+
fn mk_pos(line: Int, column: Int, offset: Int) -> Position {
67+
#{ line: line, column: column, offset: offset }
68+
}
69+
70+
fn mk_loc(start_off: Int, end_off: Int, file: String) -> Location {
71+
#{ start: mk_pos(0, 0, start_off), end_: mk_pos(0, 0, end_off), file: file }
72+
}
73+
74+
fn code_at_str(s: String, i: Int) -> Int {
75+
match char_at(s, i) {
76+
Some(c) => char_to_int(c),
77+
None => -1
78+
}
79+
}
80+
81+
fn is_ws_code(c: Int) -> Bool { c == 32 || c == 9 || c == 13 }
82+
83+
// ---- source reconstruction (round-trip) ----
84+
85+
fn trivia_text(t: Trivia) -> String { t.text }
86+
87+
pub fn to_source(node: CstNode) -> String {
88+
match node {
89+
NodeToken(tok) => {
90+
let leading = join(map(tok.leadingTrivia, trivia_text), "");
91+
let trailing = join(map(tok.trailingTrivia, trivia_text), "");
92+
leading ++ tok.text ++ trailing
93+
},
94+
NodeTree(tree) => tree_to_source(tree)
95+
}
96+
}
97+
98+
pub fn tree_to_source(tree: CstTree) -> String {
99+
join(map(tree.children, to_source), "")
100+
}
101+
102+
// ---- token collection (document order) ----
103+
104+
fn node_tokens(node: CstNode) -> [CstToken] {
105+
match node {
106+
NodeToken(tok) => [tok],
107+
NodeTree(subtree) => tokens(subtree)
108+
}
109+
}
110+
111+
pub fn tokens(tree: CstTree) -> [CstToken] {
112+
flat_map(node_tokens, tree.children)
113+
}
114+
115+
// ---- trivia classification (gap of source -> trivia items) ----
116+
117+
pub fn classify_trivia(gap: String, file: String, base_offset: Int) -> [Trivia] {
118+
let mut result = [];
119+
let mut i = 0;
120+
let glen = length(gap);
121+
while i < glen {
122+
let c = code_at_str(gap, i);
123+
if c == 35 { // '#': line comment to end of line
124+
let start = i;
125+
let mut j = i;
126+
while (j < glen) && (code_at_str(gap, j) != 10) { j = j + 1; }
127+
let loc = mk_loc(base_offset + start, base_offset + j, file);
128+
result = result ++ [#{ kind: TkLineComment, text: substring(gap, start, j), loc: loc }];
129+
i = j;
130+
} else if c == 10 { // newline
131+
let loc = mk_loc(base_offset + i, base_offset + i + 1, file);
132+
result = result ++ [#{ kind: TkNewline, text: "\n", loc: loc }];
133+
i = i + 1;
134+
} else if is_ws_code(c) { // contiguous whitespace
135+
let start = i;
136+
let mut j = i;
137+
while (j < glen) && is_ws_code(code_at_str(gap, j)) { j = j + 1; }
138+
let loc = mk_loc(base_offset + start, base_offset + j, file);
139+
result = result ++ [#{ kind: TkWhitespace, text: substring(gap, start, j), loc: loc }];
140+
i = j;
141+
} else { // anything else (shouldn't occur in gaps)
142+
let loc = mk_loc(base_offset + i, base_offset + i + 1, file);
143+
result = result ++ [#{ kind: TkWhitespace, text: substring(gap, i, i + 1), loc: loc }];
144+
i = i + 1;
145+
}
146+
}
147+
result
148+
}
149+
150+
// ---- trivia-aware lexing ----
151+
152+
struct LexAcc {
153+
committed: [CstToken],
154+
last: Option<CstToken>,
155+
prev_end: Int
156+
}
157+
158+
fn set_trailing(t: CstToken, trailing: [Trivia]) -> CstToken {
159+
#{ tokenKind: t.tokenKind, text: t.text, leadingTrivia: t.leadingTrivia,
160+
trailingTrivia: trailing, loc: t.loc }
161+
}
162+
163+
pub fn lex_with_trivia(source: String, file: String, run_number: Int) -> [CstToken] {
164+
let (raw_tokens, _diags) = lex(source, file, run_number);
165+
166+
let final = fold(raw_tokens, #{ committed: [], last: None, prev_end: 0 }, |acc, tok| {
167+
let tok_start = tok.loc.start.offset;
168+
let tok_end = tok.loc.end_.offset;
169+
let leading = if tok_start > acc.prev_end {
170+
classify_trivia(substring(source, acc.prev_end, tok_start), file, acc.prev_end)
171+
} else {
172+
[]
173+
};
174+
match tok.type_ {
175+
Newline => {
176+
let nl = #{ kind: TkNewline, text: tok.lexeme, loc: tok.loc };
177+
match acc.last {
178+
None => {
179+
let synth = #{ tokenKind: tok.type_, text: tok.lexeme, leadingTrivia: leading,
180+
trailingTrivia: [], loc: tok.loc };
181+
#{ committed: acc.committed, last: Some(synth), prev_end: tok_end }
182+
},
183+
Some(lt) => {
184+
let updated = set_trailing(lt, lt.trailingTrivia ++ leading ++ [nl]);
185+
#{ committed: acc.committed, last: Some(updated), prev_end: tok_end }
186+
}
187+
}
188+
},
189+
_ => {
190+
let new_committed = match acc.last {
191+
Some(lt) => acc.committed ++ [lt],
192+
None => acc.committed
193+
};
194+
let new_tok = #{ tokenKind: tok.type_, text: tok.lexeme, leadingTrivia: leading,
195+
trailingTrivia: [], loc: tok.loc };
196+
#{ committed: new_committed, last: Some(new_tok), prev_end: tok_end }
197+
}
198+
}
199+
});
200+
201+
// attach trailing source after the last token, then commit the last token
202+
let total_len = length(source);
203+
match final.last {
204+
None => final.committed,
205+
Some(lt) => {
206+
let lt2 = if final.prev_end < total_len {
207+
set_trailing(lt, lt.trailingTrivia ++ classify_trivia(substring(source, final.prev_end, total_len), file, final.prev_end))
208+
} else {
209+
lt
210+
};
211+
final.committed ++ [lt2]
212+
}
213+
}
214+
}
215+
216+
// ---- public API ----
217+
218+
pub fn parse_to_cst(source: String, file: String, run_number: Int) -> CstTree {
219+
let cst_tokens = lex_with_trivia(source, file, run_number);
220+
let children = map(cst_tokens, |tok| NodeToken(tok));
221+
let n = len(cst_tokens);
222+
let loc = if n == 0 {
223+
mk_loc(0, 0, file)
224+
} else {
225+
#{ start: cst_tokens[0].loc.start, end_: cst_tokens[n - 1].loc.end_, file: file }
226+
};
227+
#{ kind: SourceFile, children: children, loc: loc }
228+
}

0 commit comments

Comments
 (0)