Skip to content

Commit 6f43dc7

Browse files
committed
feat(types): add Echo types (structured loss) across the pipeline
Introduce `Echo<A, B>` — the fibre of a function A → B (a retained witness `x : A` with `f x ≡ y : B`) — and its residue `EchoR<A, B>`, porting the echo-types (Agda) / EchoTypes.jl lineage into Error-Lang as a runnable, stability-aware model of non-total erasure. Pipeline: - Types/Lexer: `Echo`/`EchoR` keywords; `TyEcho`/`TyEchoResidue` typeExpr with sugar (`Echo<A,B>`, `Echo<A>`, bare `Echo`). - Parser: real `parseTypeExpr` (primitives, `Array<T>`, Echo/EchoR, nested `>>` splitting) wired into let annotations, struct fields and function params/return — annotations were previously dropped. - TypeChecker: internal `TyEcho`/`TyEchoR` with conversion, display and unification. Echo and EchoR unify only with their own kind and never with each other, so erasure is irreversible at the type level. Builtins mirror EchoTypes.jl: echo, echo_to_residue, residue_strictly_loses, echo_input (illegal on a residue), echo_output. - Runtime (Bytecode/VM/Codegen): `VEcho{input,output}` single-witness value and `VResidue{output}`; dedicated opcodes. Erasing an Echo to its residue debits stability (a Landauer-style cost; cf. fiber_erasure_bound). - Pretty/LayerNavigator: render the new type forms. - spec/type-system.md: new §7 (Echo Types) plus the [Stab-Erase] rule. - Tests: type-checker, parser and lexer coverage for Echo/EchoR. The core conversion/display/unification logic was compile-verified and behaviourally checked in isolation (the wider compiler/ tree does not build against mainstream ReScript for reasons predating this change).
1 parent 8751904 commit 6f43dc7

13 files changed

Lines changed: 691 additions & 33 deletions

compiler/src/Bytecode.res

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ type opcode =
5050
| OpUpdateStability // Recalculate stability score
5151
| OpInjectParadox(paradoxType) // Activate a paradox
5252

53+
// Echo types (structured loss)
54+
| OpEcho // Pop output then input, push a fiber witness VEcho{input, output}
55+
| OpEchoToResidue // Pop an echo, push its residue (erases the witness; costs stability)
56+
| OpResidueStrictlyLoses // Pop a value, push VBool(true) iff it is a residue
57+
| OpEchoInput // Pop an echo, push its input witness (runtime error on a residue)
58+
| OpEchoOutput // Pop an echo or residue, push its retained output
59+
5360
// Debug
5461
| OpPrint(bool) // Print (println if true)
5562
| OpHalt // Stop execution
@@ -86,6 +93,11 @@ and value =
8693
| VNil
8794
| VArray(array<value>)
8895
| VFunction({arity: int, address: int, name: string})
96+
// A single fiber witness: input `x` reached output `y` (one element of `Echo<A, B>`).
97+
| VEcho({input: value, output: value})
98+
// The residue of an echo after erasure: the input witness is gone (non-recoverable),
99+
// only the reached output is retained. This is `EchoR<A, B>` at runtime.
100+
| VResidue({output: value})
89101

90102
// Compiled bytecode chunk
91103
type chunk = {
@@ -119,6 +131,8 @@ let valueToString = (v: value): string => {
119131
`[${items}]`
120132
}
121133
| VFunction({name}) => `<function ${name}>`
134+
| VEcho({input, output}) => `echo(${valueToString(input)} ↦ ${valueToString(output)})`
135+
| VResidue({output}) => `residue(↦ ${valueToString(output)})`
122136
}
123137
}
124138

@@ -156,6 +170,11 @@ let opcodeToString = (op: opcode): string => {
156170
| OpCheckpoint(name) => `CHECKPOINT "${name}"`
157171
| OpUpdateStability => "UPDATE_STABILITY"
158172
| OpInjectParadox(_) => "INJECT_PARADOX"
173+
| OpEcho => "ECHO"
174+
| OpEchoToResidue => "ECHO_TO_RESIDUE"
175+
| OpResidueStrictlyLoses => "RESIDUE_STRICTLY_LOSES"
176+
| OpEchoInput => "ECHO_INPUT"
177+
| OpEchoOutput => "ECHO_OUTPUT"
159178
| OpPrint(println) => println ? "PRINTLN" : "PRINT"
160179
| OpHalt => "HALT"
161180
}

compiler/src/Codegen.res

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,35 @@ let rec compileExpr = (c: compiler, expr: expr): unit => {
130130
| BNot => () // Not implemented
131131
}
132132
}
133-
| Call(_func, _args, loc) => {
134-
// Function calls not fully implemented yet
135-
emit(c, OpPush(VNil), loc)
133+
| Call(callee, args, loc) => {
134+
// Echo builtins compile to dedicated opcodes (the VM has no general call yet).
135+
// Arguments are pushed left-to-right; for `echo(x, y)` the output `y` is on top,
136+
// which is exactly what OpEcho pops first.
137+
switch callee {
138+
| Ident("echo", _) => {
139+
args->Array.forEach(a => compileExpr(c, a))
140+
emit(c, OpEcho, loc)
141+
}
142+
| Ident("echo_to_residue", _) => {
143+
args->Array.forEach(a => compileExpr(c, a))
144+
emit(c, OpEchoToResidue, loc)
145+
}
146+
| Ident("residue_strictly_loses", _) => {
147+
args->Array.forEach(a => compileExpr(c, a))
148+
emit(c, OpResidueStrictlyLoses, loc)
149+
}
150+
| Ident("echo_input", _) => {
151+
args->Array.forEach(a => compileExpr(c, a))
152+
emit(c, OpEchoInput, loc)
153+
}
154+
| Ident("echo_output", _) => {
155+
args->Array.forEach(a => compileExpr(c, a))
156+
emit(c, OpEchoOutput, loc)
157+
}
158+
| _ =>
159+
// Other function calls not fully implemented yet
160+
emit(c, OpPush(VNil), loc)
161+
}
136162
}
137163
| Index(array, index, loc) => {
138164
compileExpr(c, array)

compiler/src/LayerNavigator.res

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ let typeExprToString = (t: typeExpr): string => {
292292
| TyString => "String"
293293
| TyBool => "Bool"
294294
| TyArray(inner) => `Array<${typeExprToString(inner)}>`
295+
| TyEcho(a, b) =>
296+
switch (a, b) {
297+
| (None, _) => "Echo"
298+
| (Some(ta), None) => `Echo<${typeExprToString(ta)}>`
299+
| (Some(ta), Some(tb)) => `Echo<${typeExprToString(ta)}, ${typeExprToString(tb)}>`
300+
}
301+
| TyEchoResidue(a, b) =>
302+
switch (a, b) {
303+
| (None, _) => "EchoR"
304+
| (Some(ta), None) => `EchoR<${typeExprToString(ta)}>`
305+
| (Some(ta), Some(tb)) => `EchoR<${typeExprToString(ta)}, ${typeExprToString(tb)}>`
306+
}
295307
| TyIdent(name) => name
296308
}
297309
}

compiler/src/Lexer.res

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ let keywords: Dict.t<tokenType> = Dict.fromArray([
133133
("String", TString),
134134
("Bool", TBool),
135135
("Array", TArray),
136+
("Echo", TEcho),
137+
("EchoR", TEchoR),
136138
("print", Identifier("print")),
137139
("println", Identifier("println")),
138140
])

compiler/src/Parser.res

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,73 @@ and parsePrimary = (state: parserState): option<expr> => {
552552
}
553553
}
554554

555+
// ============================================
556+
// Type Annotation Parsing
557+
// ============================================
558+
559+
// Consume a closing '>' for a type-argument list. Because the lexer greedily
560+
// produces `>>` as a single `GreaterGreater` token, a closing angle that sits
561+
// directly against an enclosing one (e.g. `Echo<Array<Int>>`) arrives fused.
562+
// We split it in place: consume one '>' worth and leave a `Greater` behind for
563+
// the enclosing type to close against.
564+
let rec expectCloseAngle = (state: parserState): unit => {
565+
switch peek(state) {
566+
| Some({type_: Greater}) => advance(state)->ignore
567+
| Some({type_: GreaterGreater, loc}) =>
568+
state.tokens[state.pos] = {type_: Greater, lexeme: ">", loc}
569+
| Some(tok) => addDiagnostic(state, E0006, "Expected '>' to close type arguments", tok.loc)
570+
| None => ()
571+
}
572+
}
573+
574+
// Parse an optional `<A>` or `<A, B>` argument list. Returns (first, second).
575+
and parseTypeArgs = (state: parserState): (option<typeExpr>, option<typeExpr>) => {
576+
if check(state, Less) {
577+
advance(state)->ignore
578+
let first = parseTypeExpr(state)
579+
let second = if match_(state, [Comma]) {
580+
parseTypeExpr(state)
581+
} else {
582+
None
583+
}
584+
expectCloseAngle(state)
585+
(first, second)
586+
} else {
587+
(None, None)
588+
}
589+
}
590+
591+
// Parse a type expression: primitives, Array<T>, Echo / Echo<A> / Echo<A, B>,
592+
// EchoR / EchoR<A> / EchoR<A, B>, or a named identifier type.
593+
and parseTypeExpr = (state: parserState): option<typeExpr> => {
594+
switch peek(state) {
595+
| Some({type_: TInt}) => { advance(state)->ignore; Some(TyInt) }
596+
| Some({type_: TFloat}) => { advance(state)->ignore; Some(TyFloat) }
597+
| Some({type_: TString}) => { advance(state)->ignore; Some(TyString) }
598+
| Some({type_: TBool}) => { advance(state)->ignore; Some(TyBool) }
599+
| Some({type_: TArray}) => {
600+
advance(state)->ignore
601+
switch parseTypeArgs(state) {
602+
| (Some(inner), _) => Some(TyArray(inner))
603+
// Bare `Array` with no parameter: default the element type to `Any`.
604+
| (None, _) => Some(TyArray(TyIdent("Any")))
605+
}
606+
}
607+
| Some({type_: TEcho}) => {
608+
advance(state)->ignore
609+
let (a, b) = parseTypeArgs(state)
610+
Some(TyEcho(a, b))
611+
}
612+
| Some({type_: TEchoR}) => {
613+
advance(state)->ignore
614+
let (a, b) = parseTypeArgs(state)
615+
Some(TyEchoResidue(a, b))
616+
}
617+
| Some({type_: Identifier(name)}) => { advance(state)->ignore; Some(TyIdent(name)) }
618+
| _ => None
619+
}
620+
}
621+
555622
// ============================================
556623
// Statement Parsing
557624
// ============================================
@@ -567,12 +634,17 @@ let parseStatement = (state: parserState): option<stmt> => {
567634
switch peek(state) {
568635
| Some({type_: Identifier(name)}) => {
569636
advance(state)->ignore
570-
// Optional type annotation (skip for now)
637+
// Optional type annotation: `let x: Type = ...`
638+
let type_ = if match_(state, [Colon]) {
639+
parseTypeExpr(state)
640+
} else {
641+
None
642+
}
571643
expect(state, Equal, "Expected '=' after variable name")->ignore
572644
switch parseExpression(state) {
573645
| Some(value) => {
574646
let loc = {start: startLoc.start, end_: currentLoc(state).end_, file: state.file}
575-
Some(LetStmt({mutable_, name, type_: None, value, loc}))
647+
Some(LetStmt({mutable_, name, type_, value, loc}))
576648
}
577649
| None => None
578650
}
@@ -809,14 +881,15 @@ let parseDeclaration = (state: parserState): option<decl> => {
809881
advance(state)->ignore
810882
expect(state, LParen, "Expected '(' after function name")->ignore
811883

812-
// Parse parameters
884+
// Parse parameters (each with an optional `: Type` annotation)
813885
let params = ref([])
814886
skipNewlines(state)
815887
if !check(state, RParen) {
816888
switch peek(state) {
817889
| Some({type_: Identifier(pname), loc: ploc}) => {
818890
advance(state)->ignore
819-
params := Array.concat(params.contents, [{name: pname, type_: None, loc: ploc}])
891+
let ptype = if match_(state, [Colon]) { parseTypeExpr(state) } else { None }
892+
params := Array.concat(params.contents, [{name: pname, type_: ptype, loc: ploc}])
820893
}
821894
| _ => ()
822895
}
@@ -825,20 +898,26 @@ let parseDeclaration = (state: parserState): option<decl> => {
825898
switch peek(state) {
826899
| Some({type_: Identifier(pname), loc: ploc}) => {
827900
advance(state)->ignore
828-
params := Array.concat(params.contents, [{name: pname, type_: None, loc: ploc}])
901+
let ptype = if match_(state, [Colon]) { parseTypeExpr(state) } else { None }
902+
params := Array.concat(params.contents, [{name: pname, type_: ptype, loc: ploc}])
829903
}
830904
| _ => ()
831905
}
832906
}
833907
}
834908
expect(state, RParen, "Expected ')' after parameters")->ignore
835909

836-
// Optional return type (skip for now)
910+
// Optional return type: `function f(...) -> Type`
911+
let returnType = if match_(state, [Arrow]) {
912+
parseTypeExpr(state)
913+
} else {
914+
None
915+
}
837916
skipNewlines(state)
838917

839918
let body = parseBlock(state)
840919
let loc = {start: startLoc.start, end_: currentLoc(state).end_, file: state.file}
841-
Some(FunctionDecl({name, params: params.contents, returnType: None, body, loc}))
920+
Some(FunctionDecl({name, params: params.contents, returnType, body, loc}))
842921
}
843922
| _ => {
844923
addDiagnostic(state, E0001, "Expected function name", currentLoc(state))
@@ -869,29 +948,10 @@ let parseDeclaration = (state: parserState): option<decl> => {
869948
| Some({type_: Identifier(fname)}) => {
870949
advance(state)->ignore
871950
expect(state, Colon, "Expected ':' after field name")->ignore
872-
// Simple type parsing
873-
switch peek(state) {
874-
| Some({type_: TInt}) => {
875-
advance(state)->ignore
876-
fields := Array.concat(fields.contents, [(fname, TyInt)])
877-
}
878-
| Some({type_: TFloat}) => {
879-
advance(state)->ignore
880-
fields := Array.concat(fields.contents, [(fname, TyFloat)])
881-
}
882-
| Some({type_: TString}) => {
883-
advance(state)->ignore
884-
fields := Array.concat(fields.contents, [(fname, TyString)])
885-
}
886-
| Some({type_: TBool}) => {
887-
advance(state)->ignore
888-
fields := Array.concat(fields.contents, [(fname, TyBool)])
889-
}
890-
| Some({type_: Identifier(tname)}) => {
891-
advance(state)->ignore
892-
fields := Array.concat(fields.contents, [(fname, TyIdent(tname))])
893-
}
894-
| _ => ()
951+
// Full type parsing (primitives, Array<T>, Echo<A, B>, EchoR<A, B>, idents)
952+
switch parseTypeExpr(state) {
953+
| Some(ty) => fields := Array.concat(fields.contents, [(fname, ty)])
954+
| None => ()
895955
}
896956
}
897957
| _ => ()

compiler/src/Pretty.res

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,29 @@ let rec ppTypeExpr = (p, ty) =>
6161
emit(p, "[")
6262
ppTypeExpr(p, inner)
6363
emit(p, "]")
64+
| TyEcho(a, b) => ppEchoLike(p, "Echo", a, b)
65+
| TyEchoResidue(a, b) => ppEchoLike(p, "EchoR", a, b)
6466
| TyIdent(name) => emit(p, name)
6567
}
6668

69+
// Print an Echo/EchoR head with its optional `<A>` or `<A, B>` arguments.
70+
and ppEchoLike = (p, head, a, b) => {
71+
emit(p, head)
72+
switch (a, b) {
73+
| (None, _) => ()
74+
| (Some(ta), None) =>
75+
emit(p, "<")
76+
ppTypeExpr(p, ta)
77+
emit(p, ">")
78+
| (Some(ta), Some(tb)) =>
79+
emit(p, "<")
80+
ppTypeExpr(p, ta)
81+
emit(p, ", ")
82+
ppTypeExpr(p, tb)
83+
emit(p, ">")
84+
}
85+
}
86+
6787
and ppBinaryOp = (p, op) =>
6888
emit(
6989
p,

0 commit comments

Comments
 (0)