Skip to content

Commit 2821914

Browse files
ReScript→AffineScript migration: Lexer.affine (module 2) (#27)
* 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 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 727c428 commit 2821914

1 file changed

Lines changed: 363 additions & 0 deletions

File tree

compiler/src/Lexer.affine

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Lexer.affine — tokenizer for Error-Lang (ported from compiler/src/Lexer.res).
3+
//
4+
// AffineScript has no mutable struct fields or record-spread, so the imperative
5+
// ReScript lexer is rendered functionally: state-mutating helpers take a
6+
// LexerState and return a new one (full-record reconstruction); the scanners and
7+
// driver loop a `let mut` local. Single chars are Char (char_at / char_to_int);
8+
// the escape buffer is built with single-char substrings.
9+
//
10+
// Known parity gaps vs Lexer.res (decimal/core path is faithful; these are
11+
// tracked for a follow-up parity pass):
12+
// - hex (0x) / binary (0b) integer prefixes
13+
// - triple-quoted strings and the \0 null escape
14+
// - smart-quote (U+201C / U+201D) detection (diagnostic E0007)
15+
16+
module Lexer;
17+
18+
use prelude::*;
19+
use string::{char_at, length, substring, is_digit, is_alpha, is_alphanumeric};
20+
use Types::*;
21+
22+
pub struct LexerState {
23+
source: String,
24+
file: String,
25+
pos: Int,
26+
line: Int,
27+
column: Int,
28+
tokens: [Token],
29+
diagnostics: [Diagnostic],
30+
runNumber: Int
31+
}
32+
33+
pub fn make(source: String, file: String, run_number: Int) -> LexerState {
34+
#{ source: source, file: file, pos: 0, line: 1, column: 1,
35+
tokens: [], diagnostics: [], runNumber: run_number }
36+
}
37+
38+
// ---- peeking (read copyable fields only; never consume the state) ----
39+
40+
fn peek_at(source: String, pos: Int, offset: Int) -> Option<Char> {
41+
char_at(source, pos + offset)
42+
}
43+
44+
// code point of the char at pos+offset, or -1 at end of input
45+
fn code_at(source: String, pos: Int, offset: Int) -> Int {
46+
match char_at(source, pos + offset) {
47+
Some(c) => char_to_int(c),
48+
None => -1
49+
}
50+
}
51+
52+
fn mk_pos(line: Int, column: Int, offset: Int) -> Position {
53+
#{ line: line, column: column, offset: offset }
54+
}
55+
56+
fn is_hex_digit(c: Char) -> Bool {
57+
let k = char_to_int(c);
58+
(k >= 48 && k <= 57) || (k >= 97 && k <= 102) || (k >= 65 && k <= 70)
59+
}
60+
61+
// ---- keyword table (ReScript Dict -> match on string equality) ----
62+
63+
fn keyword_lookup(w: String) -> Option<TokenType> {
64+
if w == "main" { Some(Main) }
65+
else if w == "end" { Some(End) }
66+
else if w == "let" { Some(Let) }
67+
else if w == "mutable" { Some(Mutable) }
68+
else if w == "function" { Some(Function) }
69+
else if w == "struct" { Some(Struct) }
70+
else if w == "if" { Some(If) }
71+
else if w == "elseif" { Some(Elseif) }
72+
else if w == "else" { Some(Else) }
73+
else if w == "while" { Some(While) }
74+
else if w == "for" { Some(For) }
75+
else if w == "in" { Some(In) }
76+
else if w == "break" { Some(Break) }
77+
else if w == "continue" { Some(Continue) }
78+
else if w == "return" { Some(Return) }
79+
else if w == "and" { Some(And) }
80+
else if w == "or" { Some(Or) }
81+
else if w == "not" { Some(Not) }
82+
else if w == "true" { Some(True) }
83+
else if w == "false" { Some(False) }
84+
else if w == "nil" { Some(Nil) }
85+
else if w == "gutter" { Some(Gutter) }
86+
else if w == "fn" { Some(Fn) }
87+
else if w == "Int" { Some(TInt) }
88+
else if w == "Float" { Some(TFloat) }
89+
else if w == "String" { Some(TString) }
90+
else if w == "Bool" { Some(TBool) }
91+
else if w == "Array" { Some(TArray) }
92+
else if w == "Echo" { Some(TEcho) }
93+
else if w == "EchoR" { Some(TEchoR) }
94+
else if w == "print" { Some(Identifier("print")) }
95+
else if w == "println" { Some(Identifier("println")) }
96+
else { None }
97+
}
98+
99+
// ---- state transitions (consume + rebuild) ----
100+
101+
fn advance(s: LexerState) -> LexerState {
102+
let is_nl = code_at(s.source, s.pos, 0) == 10;
103+
let new_line = if is_nl { s.line + 1 } else { s.line };
104+
let new_col = if is_nl { 1 } else { s.column + 1 };
105+
#{ source: s.source, file: s.file, pos: s.pos + 1,
106+
line: new_line, column: new_col,
107+
tokens: s.tokens, diagnostics: s.diagnostics, runNumber: s.runNumber }
108+
}
109+
110+
fn add_token(s: LexerState, type_: TokenType, lexeme: String, loc: Location) -> LexerState {
111+
let tok = #{ type_: type_, lexeme: lexeme, loc: loc };
112+
#{ source: s.source, file: s.file, pos: s.pos, line: s.line, column: s.column,
113+
tokens: s.tokens ++ [tok], diagnostics: s.diagnostics, runNumber: s.runNumber }
114+
}
115+
116+
fn add_diagnostic(s: LexerState, code: ErrorCode, message: String, loc: Location) -> LexerState {
117+
let diag = #{ code: code, message: message, loc: loc,
118+
runNumber: s.runNumber, hint: None };
119+
#{ source: s.source, file: s.file, pos: s.pos, line: s.line, column: s.column,
120+
tokens: s.tokens, diagnostics: s.diagnostics ++ [diag], runNumber: s.runNumber }
121+
}
122+
123+
// Advance once, emitting a token whose location spans the single consumed char.
124+
fn single_tok(s: LexerState, t: TokenType, lex: String) -> LexerState {
125+
let start = mk_pos(s.line, s.column, s.pos);
126+
let s2 = advance(s);
127+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
128+
add_token(s2, t, lex, loc)
129+
}
130+
131+
// ---- scanners ----
132+
133+
fn scan_comment(s: LexerState) -> LexerState {
134+
let mut st = s;
135+
while (code_at(st.source, st.pos, 0) != 10) && (st.pos < length(st.source)) {
136+
st = advance(st);
137+
}
138+
st
139+
}
140+
141+
fn scan_identifier(s: LexerState) -> LexerState {
142+
let start = mk_pos(s.line, s.column, s.pos);
143+
let start_pos = s.pos;
144+
let mut st = s;
145+
while (match char_at(st.source, st.pos) { Some(c) => is_alphanumeric(c), None => false }) {
146+
st = advance(st);
147+
}
148+
let lexeme = substring(st.source, start_pos, st.pos);
149+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
150+
let type_ = match keyword_lookup(lexeme) {
151+
Some(kw) => kw,
152+
None => Identifier(lexeme)
153+
};
154+
add_token(st, type_, lexeme, loc)
155+
}
156+
157+
fn scan_number(s: LexerState) -> LexerState {
158+
let start = mk_pos(s.line, s.column, s.pos);
159+
let start_pos = s.pos;
160+
let mut st = s;
161+
162+
// integer part
163+
while (match char_at(st.source, st.pos) { Some(c) => is_digit(c), None => false }) {
164+
st = advance(st);
165+
}
166+
167+
// optional fractional part: '.' followed by a digit
168+
let mut is_float = false;
169+
if (code_at(st.source, st.pos, 0) == 46) && (match char_at(st.source, st.pos + 1) { Some(c) => is_digit(c), None => false }) {
170+
is_float = true;
171+
st = advance(st); // '.'
172+
while (match char_at(st.source, st.pos) { Some(c) => is_digit(c), None => false }) {
173+
st = advance(st);
174+
}
175+
}
176+
177+
// optional exponent
178+
let ec = code_at(st.source, st.pos, 0);
179+
if ec == 101 || ec == 69 { // e / E
180+
is_float = true;
181+
st = advance(st);
182+
let sgn = code_at(st.source, st.pos, 0);
183+
if sgn == 43 || sgn == 45 { st = advance(st); } // + / -
184+
while (match char_at(st.source, st.pos) { Some(c) => is_digit(c), None => false }) {
185+
st = advance(st);
186+
}
187+
}
188+
189+
let lexeme = substring(st.source, start_pos, st.pos);
190+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
191+
if is_float {
192+
match parse_float(lexeme) {
193+
Some(f) => add_token(st, FloatTok(f), lexeme, loc),
194+
None => add_token(st, Error("Invalid float"), lexeme, loc)
195+
}
196+
} else {
197+
match parse_int(lexeme) {
198+
Some(n) => add_token(st, Integer(n), lexeme, loc),
199+
None => add_token(st, Error("Invalid integer"), lexeme, loc)
200+
}
201+
}
202+
}
203+
204+
// String literal (single-quote-aware double-quoted; escapes; no triple-quote —
205+
// simplified from the .res triple-quote handling, noted for the parity pass).
206+
fn scan_string(s: LexerState) -> LexerState {
207+
let start = mk_pos(s.line, s.column, s.pos);
208+
let mut st = advance(s); // opening quote
209+
let mut buf = "";
210+
let mut done = false;
211+
let mut result = st;
212+
213+
while !done {
214+
let c = code_at(st.source, st.pos, 0);
215+
if c == -1 {
216+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
217+
let st2 = add_diagnostic(st, E0002, "Unterminated string literal", loc);
218+
result = add_token(st2, Error("Unterminated string"), buf, loc);
219+
done = true;
220+
} else if c == 34 { // closing "
221+
st = advance(st);
222+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
223+
let quoted = "\"" ++ buf ++ "\"";
224+
result = add_token(st, StringTok(buf), quoted, loc);
225+
done = true;
226+
} else if c == 92 { // backslash escape
227+
st = advance(st);
228+
let e = code_at(st.source, st.pos, 0);
229+
if e == 110 { buf = buf ++ "\n"; st = advance(st); }
230+
else if e == 114 { buf = buf ++ "\r"; st = advance(st); }
231+
else if e == 116 { buf = buf ++ "\t"; st = advance(st); }
232+
else if e == 92 { buf = buf ++ "\\"; st = advance(st); }
233+
else if e == 34 { buf = buf ++ "\""; st = advance(st); }
234+
else if e == -1 { done = true; result = st; }
235+
else {
236+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
237+
st = add_diagnostic(st, E0003, "Invalid escape sequence", loc);
238+
st = advance(st);
239+
}
240+
} else if c == 10 { // newline in string
241+
let loc = #{ start: start, end_: mk_pos(st.line, st.column, st.pos), file: st.file };
242+
let st2 = add_diagnostic(st, E0002, "Unterminated string literal (newline in string)", loc);
243+
result = add_token(st2, Error("Unterminated string"), buf, loc);
244+
done = true;
245+
} else {
246+
buf = buf ++ substring(st.source, st.pos, st.pos + 1);
247+
st = advance(st);
248+
}
249+
}
250+
result
251+
}
252+
253+
// ---- main dispatch + driver ----
254+
255+
fn step(s: LexerState) -> LexerState {
256+
let c = code_at(s.source, s.pos, 0);
257+
if c == 32 || c == 9 || c == 13 {
258+
advance(s)
259+
} else if c == 10 { // newline token
260+
let start = mk_pos(s.line, s.column, s.pos);
261+
let s2 = advance(s);
262+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
263+
add_token(s2, Newline, "\\n", loc)
264+
} else if c == 35 { // #
265+
scan_comment(s)
266+
} else if c == 43 { single_tok(s, Plus, "+") }
267+
else if c == 45 { // - or ->
268+
if code_at(s.source, s.pos, 1) == 62 {
269+
let start = mk_pos(s.line, s.column, s.pos);
270+
let s2 = advance(advance(s));
271+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
272+
add_token(s2, Arrow, "->", loc)
273+
} else { single_tok(s, Minus, "-") }
274+
}
275+
else if c == 42 { single_tok(s, Star, "*") }
276+
else if c == 47 { single_tok(s, Slash, "/") }
277+
else if c == 37 { single_tok(s, Percent, "%") }
278+
else if c == 61 { // = or ==
279+
if code_at(s.source, s.pos, 1) == 61 {
280+
let start = mk_pos(s.line, s.column, s.pos);
281+
let s2 = advance(advance(s));
282+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
283+
add_token(s2, EqualEqual, "==", loc)
284+
} else { single_tok(s, Equal, "=") }
285+
}
286+
else if c == 33 { // ! -> != or error
287+
if code_at(s.source, s.pos, 1) == 61 {
288+
let start = mk_pos(s.line, s.column, s.pos);
289+
let s2 = advance(advance(s));
290+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
291+
add_token(s2, BangEqual, "!=", loc)
292+
} else {
293+
let start = mk_pos(s.line, s.column, s.pos);
294+
let s2 = advance(s);
295+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
296+
add_diagnostic(s2, E0001, "Unexpected character '!'", loc)
297+
}
298+
}
299+
else if c == 60 { // < <= <<
300+
let n = code_at(s.source, s.pos, 1);
301+
if n == 61 { two_char(s, LessEqual, "<=") }
302+
else if n == 60 { two_char(s, LessLess, "<<") }
303+
else { single_tok(s, Less, "<") }
304+
}
305+
else if c == 62 { // > >= >>
306+
let n = code_at(s.source, s.pos, 1);
307+
if n == 61 { two_char(s, GreaterEqual, ">=") }
308+
else if n == 62 { two_char(s, GreaterGreater, ">>") }
309+
else { single_tok(s, Greater, ">") }
310+
}
311+
else if c == 38 { single_tok(s, Ampersand, "&") }
312+
else if c == 124 { single_tok(s, Pipe, "|") }
313+
else if c == 94 { single_tok(s, Caret, "^") }
314+
else if c == 126 { single_tok(s, Tilde, "~") }
315+
else if c == 63 { single_tok(s, Question, "?") }
316+
else if c == 58 { single_tok(s, Colon, ":") }
317+
else if c == 40 { single_tok(s, LParen, "(") }
318+
else if c == 41 { single_tok(s, RParen, ")") }
319+
else if c == 91 { single_tok(s, LBracket, "[") }
320+
else if c == 93 { single_tok(s, RBracket, "]") }
321+
else if c == 123 { single_tok(s, LBrace, "{") }
322+
else if c == 125 { single_tok(s, RBrace, "}") }
323+
else if c == 44 { single_tok(s, Comma, ",") }
324+
else if c == 46 { single_tok(s, Dot, ".") }
325+
else if c == 34 { scan_string(s) } // "
326+
else {
327+
match char_at(s.source, s.pos) {
328+
Some(ch) =>
329+
if is_digit(ch) { scan_number(s) }
330+
else if is_alpha(ch) { scan_identifier(s) }
331+
else {
332+
let start = mk_pos(s.line, s.column, s.pos);
333+
let s2 = advance(s);
334+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
335+
let lex = substring(s2.source, start.offset, s2.pos);
336+
add_token(add_diagnostic(s2, E0004, "Illegal character", loc), Error("Illegal"), lex, loc)
337+
},
338+
None => s
339+
}
340+
}
341+
}
342+
343+
fn two_char(s: LexerState, t: TokenType, lex: String) -> LexerState {
344+
let start = mk_pos(s.line, s.column, s.pos);
345+
let s2 = advance(advance(s));
346+
let loc = #{ start: start, end_: mk_pos(s2.line, s2.column, s2.pos), file: s2.file };
347+
add_token(s2, t, lex, loc)
348+
}
349+
350+
pub fn tokenize(s0: LexerState) -> LexerState {
351+
let mut s = s0;
352+
while s.pos < length(s.source) {
353+
s = step(s);
354+
}
355+
let eof = mk_pos(s.line, s.column, s.pos);
356+
let loc = #{ start: eof, end_: mk_pos(s.line, s.column, s.pos), file: s.file };
357+
add_token(s, EOF, "", loc)
358+
}
359+
360+
pub fn lex(source: String, file: String, run_number: Int) -> ([Token], [Diagnostic]) {
361+
let s = tokenize(make(source, file, run_number));
362+
(s.tokens, s.diagnostics)
363+
}

0 commit comments

Comments
 (0)