Skip to content

Commit 198fa0a

Browse files
committed
docs+test: frame Echo as visible decomposition across three planes
Make Error-Lang's dissembling/decompositional narrative explicit and ensure both human and bot documentation recognise it: Echo is structured loss made VISIBLE, governed by the invariant "decomposition must be visible" (echo_to_residue is never a silent cast, EchoR is never an Echo with a missing field, the stability debit is never hidden). Documentation: - docs/Echo-Decomposition.adoc (new): the authoritative narrative — the three decomposition planes (syntactic, semantic/runtime, type-checking), the visibility invariant, the full contract, and the echo-types/EchoTypes.jl lineage. - docs/Design-Philosophy.adoc: new "Structured Loss" section tying Echo into consequence amplification + the stability score. - README.adoc: front-door paragraph on the decompositional framing + Echo. - spec/type-system.md §7.5: decomposition obligations + visibility invariant. - Bot-facing: 0-AI-MANIFEST.a2ml (core narrative, critical invariant, taxonomy pointers), META.a2ml (ADR-0001), NEUROSYM.a2ml (symbolic rules for the erasure cost, no-coercion, and echo_input-not-on-residue invariants). Proof/test pass across the three planes: - EchoRuntimeTest.res (new, semantic/runtime): echo builds VEcho; echo_input works on VEcho and fails on a residue; echo_output works on both; echo_to_residue yields VResidue; the witness is genuinely gone; stability is debited exactly once by erasure and never by projection. - TypeCheckerTest.res: EchoR does not unify back into Echo; no implicit Echo->B coercion; echo_output on a residue yields the retained output. - ParserTest.res: nested Echo<Echo<...>> (>> splitting); malformed Echo raises a diagnostic.
1 parent 6f43dc7 commit 198fa0a

10 files changed

Lines changed: 443 additions & 3 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ author = "Jonathan D.A. Jewell (hyperpolymath)"
1212

1313
[architecture-decisions]
1414
decisions = [
15-
# No ADRs recorded
15+
# ADR-0001: Echo types model structured loss as first-class, VISIBLE decomposition.
16+
# Echo<A,B> = retained witness + visible output; EchoR<A,B> = residue after
17+
# irreversible erasure; echo_to_residue is an explicit decomposition step that
18+
# debits stability ([Stab-Erase]). Invariant: decomposition must be visible —
19+
# no silent cast, EchoR is not Echo-with-a-missing-field, debit not hidden.
20+
# Runtime is single-witness (VEcho{input,output} / VResidue{output}); whole-fibre
21+
# waits for first-class functions. Builtin names mirror EchoTypes.jl.
22+
# Refs: docs/Echo-Decomposition.adoc, spec/type-system.md §7.
23+
{ id = "ADR-0001", title = "Echo types as visible structured-loss decomposition", status = "accepted" },
1624
]
1725

1826
[development-practices]

.machine_readable/6a2/NEUROSYM.a2ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ report-format = "logtalk"
1414
[symbolic-rules]
1515
# Custom symbolic rules for this project
1616
# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" }
17+
#
18+
# Echo decomposition must remain VISIBLE (see docs/Echo-Decomposition.adoc):
19+
# - echo_to_residue must debit stability — it is never a silent cast
20+
# - EchoR<A,B> must not unify with / be coerced to Echo<A,B>
21+
# - echo_input must be rejected on a residue (the witness is gone)
22+
rules = [
23+
{ name = "echo-erasure-must-cost", concern = "echo_to_residue must apply the [Stab-Erase] stability debit", severity = "high" },
24+
{ name = "echo-residue-no-coercion", concern = "no implicit Echo->EchoR or Echo->B coercion; EchoR does not unify back into Echo", severity = "high" },
25+
{ name = "echo-input-not-on-residue", concern = "echo_input is illegal on EchoR (non-recoverable residue)", severity = "high" },
26+
]
1727

1828
[neural-config]
1929
# Neural pattern detection settings

0-AI-MANIFEST.a2ml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,29 @@ project: error-lang
99
type: programming-language
1010
owner: hyperpolymath
1111
description: >
12-
Error-first programming language with graduated error levels and ReScript playground compiler.
12+
A dissembling / decompositional educational programming language. Programs,
13+
syntax, semantics, and types can decay, decompose, and destabilise over time,
14+
and the language EXPOSES that decomposition (consequence amplification +
15+
stability scoring) rather than hiding it. Error-first with graduated error
16+
levels and a ReScript playground compiler.
1317
license: MPL-2.0
1418

19+
## Core Narrative (read before reasoning about features)
20+
21+
Error-Lang is not merely "a language with errors." It is decompositional:
22+
the distinctive claim is that decomposition is itself something the language
23+
exposes and reasons about. Echo types make STRUCTURED LOSS first-class:
24+
- Echo<A, B> retained witness + visible output (a fibre witness)
25+
- EchoR<A, B> residue after irreversible erasure (witness gone)
26+
- echo_to_residue explicit decomposition step; debits stability ([Stab-Erase])
27+
Governing invariant: DECOMPOSITION MUST BE VISIBLE — echo_to_residue is never a
28+
silent cast, EchoR is never an Echo with a missing field, and the stability
29+
debit is never hidden. See docs/Echo-Decomposition.adoc and spec/type-system.md §7.
30+
1531
## Critical Invariants
1632

33+
- Decomposition must be visible: never make echo_to_residue a silent cast,
34+
never let EchoR behave like Echo, never hide the erasure stability debit
1735
- SCM files ONLY in `.machine_readable/` (root copies are symlinks)
1836
- NEVER delete spec files: grammar, SPEC.core.scm
1937
- NEVER use banned languages: TypeScript, Node.js, npm, Go, Python
@@ -58,6 +76,8 @@ license: MPL-2.0
5876
- `playground/compiler/src/lexer/Lexer.res` — @taxonomy: compiler/lexer
5977
- `playground/compiler/src/parser/Parser.res` — @taxonomy: compiler/parser
6078
- `editors/error-lang.tmLanguage.json` — @taxonomy: editors/textmate-grammar
79+
- `docs/Echo-Decomposition.adoc` — @taxonomy: docs/echo-decomposition
80+
- `spec/type-system.md` — @taxonomy: spec/type-system
6181

6282
## Session Startup Sequence
6383

README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ Error-Lang makes programming learnable the way crafts are learnable - through di
6161

6262
Unlike traditional teaching languages that hide complexity, Error-Lang **exposes complexity** and makes it explorable through **computational haptics** - visual feedback that lets you _feel_ code quality like a craftsperson feels their materials.
6363

64+
Error-Lang is a *dissembling*, *decompositional* language: programs, syntax, semantics, and types can decay, decompose, and destabilise over time — and the language makes that decomposition **visible** rather than hiding it. Its `Echo` types give *structured loss* a first-class shape (`Echo<A,B>` retained witness + visible output; `EchoR<A,B>` the non-recoverable residue after `echo_to_residue`, which debits stability). The governing rule is **decomposition must be visible**. See `docs/Echo-Decomposition.adoc`.
65+
6466
=== The Craftsperson Analogy
6567

6668
[quote, The Design Philosophy]

compiler/test/EchoRuntimeTest.res

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// EchoRuntimeTest.res — semantic / runtime decomposition plane for Echo types.
3+
//
4+
// Proves the runtime half of "decomposition must be visible":
5+
// - echo(x, y) builds VEcho{input, output}
6+
// - echo_input works on VEcho, FAILS on a residue (witness genuinely gone)
7+
// - echo_output works on BOTH VEcho and VResidue (output survives)
8+
// - echo_to_residue yields VResidue and debits stability EXACTLY ONCE
9+
// - projection (echo_input / echo_output) never debits stability
10+
11+
open Bytecode
12+
open TestHelpers
13+
14+
let dummyLoc: Types.location = {
15+
start: {line: 1, column: 1, offset: 0},
16+
end_: {line: 1, column: 1, offset: 0},
17+
file: "<echo-runtime-test>",
18+
}
19+
20+
// Build a chunk from a bare opcode list (locations unused by these opcodes).
21+
let chunkOf = (code: array<opcode>): chunk => {
22+
code,
23+
constants: [],
24+
locations: [],
25+
}
26+
27+
// Run a chunk; return (result, final stability score).
28+
let runChunk = (code: array<opcode>): (result<value, string>, float) => {
29+
let vm = VM.make(chunkOf(code))
30+
let r = VM.run(vm)
31+
(r, vm.stabilityScore)
32+
}
33+
34+
// echo(1, "a") — input pushed first, output second (OpEcho pops output then input).
35+
let echoProgram: array<opcode> = [OpPush(VInt(1)), OpPush(VString("a")), OpEcho]
36+
37+
let testEchoConstructsWitness = () => {
38+
suite("Echo runtime: construction")
39+
let (r, _) = runChunk(Array.concat(echoProgram, [OpHalt]))
40+
assertEqual("echo(1, \"a\") builds VEcho{input:1, output:\"a\"}", r, Ok(VEcho({input: VInt(1), output: VString("a")})))
41+
}
42+
43+
let testProjections = () => {
44+
suite("Echo runtime: projection")
45+
let (inp, _) = runChunk(Array.concat(echoProgram, [OpEchoInput, OpHalt]))
46+
assertEqual("echo_input recovers the witness", inp, Ok(VInt(1)))
47+
48+
let (out, _) = runChunk(Array.concat(echoProgram, [OpEchoOutput, OpHalt]))
49+
assertEqual("echo_output recovers the output", out, Ok(VString("a")))
50+
}
51+
52+
let testToResidue = () => {
53+
suite("Echo runtime: decomposition to residue")
54+
let (res, _) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpHalt]))
55+
assertEqual("echo_to_residue yields VResidue{output:\"a\"}", res, Ok(VResidue({output: VString("a")})))
56+
57+
// Output still recoverable from the residue.
58+
let (out, _) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpEchoOutput, OpHalt]))
59+
assertEqual("echo_output survives erasure", out, Ok(VString("a")))
60+
61+
// residue_strictly_loses reports non-recoverability for a residue, not for an echo.
62+
let (lossR, _) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpResidueStrictlyLoses, OpHalt]))
63+
assertEqual("residue_strictly_loses(residue) = true", lossR, Ok(VBool(true)))
64+
let (lossE, _) = runChunk(Array.concat(echoProgram, [OpResidueStrictlyLoses, OpHalt]))
65+
assertEqual("residue_strictly_loses(echo) = false", lossE, Ok(VBool(false)))
66+
}
67+
68+
let testWitnessActuallyGone = () => {
69+
suite("Echo runtime: witness is genuinely unavailable after erasure")
70+
// echo_input on a residue must be a runtime error, not silently nil.
71+
let (r, _) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpEchoInput, OpHalt]))
72+
switch r {
73+
| Error(_) => assertTrue("echo_input on residue fails at runtime", true)
74+
| Ok(_) => assertTrue("echo_input on residue fails at runtime", false)
75+
}
76+
}
77+
78+
let testStabilityDebitedExactlyOnce = () => {
79+
suite("Echo runtime: stability debit is visible and charged exactly once")
80+
// No erasure → no debit.
81+
let (_, sPlain) = runChunk(Array.concat(echoProgram, [OpEchoOutput, OpHalt]))
82+
assertEqual("projection alone does not debit stability", sPlain, 100.0)
83+
84+
// One erasure → exactly one debit (echoEraseCost = 15.0).
85+
let (_, sErase) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpHalt]))
86+
assertEqual("echo_to_residue debits stability once", sErase, 100.0 -. VM.echoEraseCost)
87+
88+
// Erasure then projection → still exactly one debit (projection is free).
89+
let (_, sEraseThenProj) = runChunk(Array.concat(echoProgram, [OpEchoToResidue, OpEchoOutput, OpHalt]))
90+
assertEqual("projection after erasure adds no further debit", sEraseThenProj, 100.0 -. VM.echoEraseCost)
91+
}
92+
93+
let runAll = () => {
94+
Console.log("\n========================================")
95+
Console.log(" ERROR-LANG ECHO RUNTIME TESTS")
96+
Console.log("========================================")
97+
testEchoConstructsWitness()
98+
testProjections()
99+
testToResidue()
100+
testWitnessActuallyGone()
101+
testStabilityDebitedExactlyOnce()
102+
summarize()
103+
}
104+
105+
let _ = runAll()

compiler/test/ParserTest.res

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,22 @@ let testEchoAnnotations = () => {
11701170
}
11711171
| _ => assertTrue("single-arg echo annotated let parsed", false)
11721172
}
1173+
1174+
// Nested form: the closing `>>` must be split, not read as a shift token.
1175+
let prog5 = parseSource("let e: Echo<Echo<Int, String>> = 1")
1176+
switch prog5.declarations[0] {
1177+
| Some(StmtDecl(LetStmt({type_}))) =>
1178+
switch type_ {
1179+
| Some(TyEcho(Some(TyEcho(Some(TyInt), Some(TyString))), None)) =>
1180+
assertTrue("nested Echo<Echo<Int, String>> parses (>> split)", true)
1181+
| _ => assertTrue("nested Echo<Echo<Int, String>> parses (>> split)", false)
1182+
}
1183+
| _ => assertTrue("nested echo annotated let parsed", false)
1184+
}
1185+
1186+
// Malformed Echo annotation must fail clearly (a diagnostic is raised).
1187+
let (_, diags) = parseWithDiagnostics("let e: Echo<Int = 1")
1188+
assertTrue("unclosed Echo type argument raises a diagnostic", Array.length(diags) > 0)
11731189
}
11741190

11751191
// ============================================

compiler/test/TypeCheckerTest.res

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,75 @@ let testResidueStrictlyLoses = () => {
648648
assertNoErrors(result, "residue_strictly_loses returns Bool")
649649
}
650650

651+
let testResidueDoesNotUnifyBackIntoEcho = () => {
652+
// let e: Echo<Int, String> = echo_to_residue(echo(1, "a")) — residue is not an Echo
653+
let prog: program = {
654+
declarations: [
655+
StmtDecl(
656+
LetStmt({
657+
mutable_: false,
658+
name: "e",
659+
type_: Some(Types.TyEcho(Some(Types.TyInt), Some(Types.TyString))),
660+
value: Call(
661+
Ident("echo_to_residue", dummyLoc),
662+
[echoCall(IntLit(1, dummyLoc), StringLit("a", dummyLoc))],
663+
dummyLoc,
664+
),
665+
loc: dummyLoc,
666+
}),
667+
),
668+
],
669+
loc: dummyLoc,
670+
}
671+
let result = checkProgram(prog)
672+
assertHasErrors(result, "EchoR does not unify back into Echo")
673+
}
674+
675+
let testNoImplicitEchoToOutputCoercion = () => {
676+
// let s: String = echo(1, "a") — an Echo is not its output; no implicit Echo -> B
677+
let prog: program = {
678+
declarations: [
679+
StmtDecl(
680+
LetStmt({
681+
mutable_: false,
682+
name: "s",
683+
type_: Some(Types.TyString),
684+
value: echoCall(IntLit(1, dummyLoc), StringLit("a", dummyLoc)),
685+
loc: dummyLoc,
686+
}),
687+
),
688+
],
689+
loc: dummyLoc,
690+
}
691+
let result = checkProgram(prog)
692+
assertHasErrors(result, "no implicit Echo<A,B> -> B coercion")
693+
}
694+
695+
let testEchoOutputOnResidue = () => {
696+
// let s: String = echo_output(echo_to_residue(echo(1, "a"))) — output survives erasure
697+
let residue = Call(
698+
Ident("echo_to_residue", dummyLoc),
699+
[echoCall(IntLit(1, dummyLoc), StringLit("a", dummyLoc))],
700+
dummyLoc,
701+
)
702+
let prog: program = {
703+
declarations: [
704+
StmtDecl(
705+
LetStmt({
706+
mutable_: false,
707+
name: "s",
708+
type_: Some(Types.TyString),
709+
value: Call(Ident("echo_output", dummyLoc), [residue], dummyLoc),
710+
loc: dummyLoc,
711+
}),
712+
),
713+
],
714+
loc: dummyLoc,
715+
}
716+
let result = checkProgram(prog)
717+
assertNoErrors(result, "echo_output on a residue yields the retained output type")
718+
}
719+
651720
// ============================================
652721
// Run All Tests
653722
// ============================================
@@ -713,6 +782,9 @@ let runAllTests = () => {
713782
testEchoInputOnResidueFails()
714783
testEchoOutputRetained()
715784
testResidueStrictlyLoses()
785+
testResidueDoesNotUnifyBackIntoEcho()
786+
testNoImplicitEchoToOutputCoercion()
787+
testEchoOutputOnResidue()
716788

717789
Console.log("")
718790
Console.log("=== Tests Complete ===")

docs/Design-Philosophy.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,36 @@ end
115115

116116
**Learning outcome:** State persists. Programs have history.
117117

118+
== Structured Loss: Decomposition Made Visible (Echo)
119+
120+
The contradictions above show *meaning* decaying. Echo types show *information*
121+
decaying — but **deliberately, observably, and irreversibly**.
122+
123+
Error-Lang is a dissembling, decompositional language: programs, syntax,
124+
semantics, and types can decay or destabilise, and the language exposes that
125+
decomposition rather than hiding it. Echo gives that idea a first-class shape:
126+
127+
[cols="1,3",options="header"]
128+
|===
129+
| Form | Meaning
130+
| `Echo<A, B>` | a retained witness `x : A` plus the visible output `y : B` it reached
131+
| `EchoR<A, B>` | the residue after irreversible erasure — the witness is gone, only the output survives
132+
| `echo_to_residue(e)` | the explicit decomposition step that destroys the witness and **debits stability**
133+
|===
134+
135+
The governing invariant is **decomposition must be visible**: `echo_to_residue`
136+
is never a silent cast, `EchoR` is not an `Echo` with a missing field, and the
137+
stability debit (consequence amplification, applied via the `[Stab-Erase]` rule)
138+
is never hidden. A learner should be able to see the exact moment structured loss
139+
becomes non-recoverable residue.
140+
141+
**Learning outcome:** Loss can be structured — but structure is not free, and the
142+
type system refuses to let you pretend the residue is still recoverable.
143+
144+
See `docs/Echo-Decomposition.adoc` for the full narrative and the three
145+
decomposition planes (syntactic, semantic/runtime, type-checking), and
146+
`spec/type-system.md` §7 for the formal rules.
147+
118148
== The Paradigm Shift
119149

120150
After Error-Lang, students will think differently:

0 commit comments

Comments
 (0)