fitch-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.OPERATORS = void 0;
7
+ exports.parseFormula = parseFormula;
8
+ /**
9
+ * Formula parser for Fitch-style proofs
10
+ */
11
+
12
+ var OPERATORS = exports.OPERATORS = {
13
+ AND: '∧',
14
+ OR: '∨',
15
+ NOT: '¬',
16
+ IMPLIES: '→',
17
+ IFF: '↔',
18
+ FORALL: '∀',
19
+ EXISTS: '∃',
20
+ EQUALS: '=',
21
+ CONTRA: '⊥'
22
+ };
23
+
24
+ // Pre-compute operator set for O(1) lookup
25
+ var OPERATOR_SET = new Set(Object.values(OPERATORS));
26
+
27
+ /**
28
+ * Tokenizes a formula string into an array of tokens
29
+ * @param {string} formula - The formula string to tokenize
30
+ * @returns {string[]} Array of tokens
31
+ */
32
+ function tokenize(formula) {
33
+ var tokens = [];
34
+ var chars = [];
35
+ for (var i = 0; i < formula.length; i++) {
36
+ var _char = formula[i];
37
+ if (_char === ' ') {
38
+ if (chars.length > 0) {
39
+ tokens.push(chars.join(''));
40
+ chars.length = 0;
41
+ }
42
+ continue;
43
+ }
44
+ if ('(),'.includes(_char)) {
45
+ if (chars.length > 0) {
46
+ tokens.push(chars.join(''));
47
+ chars.length = 0;
48
+ }
49
+ tokens.push(_char);
50
+ continue;
51
+ }
52
+ if (OPERATOR_SET.has(_char)) {
53
+ if (chars.length > 0) {
54
+ tokens.push(chars.join(''));
55
+ chars.length = 0;
56
+ }
57
+ tokens.push(_char);
58
+ continue;
59
+ }
60
+ chars.push(_char);
61
+ }
62
+ if (chars.length > 0) {
63
+ tokens.push(chars.join(''));
64
+ }
65
+ return tokens;
66
+ }
67
+
68
+ /**
69
+ * Parses a formula string into a Formula object
70
+ * @param {string} formulaStr - The formula string to parse
71
+ * @returns {import('./types').Formula} The parsed formula
72
+ */
73
+ function parseFormula(formulaStr) {
74
+ var tokens = tokenize(formulaStr);
75
+ var pos = 0;
76
+ function parseAtom() {
77
+ var token = tokens[pos];
78
+ pos++;
79
+
80
+ // Check for contradiction
81
+ if (token === OPERATORS.CONTRA) {
82
+ return {
83
+ type: 'splat'
84
+ };
85
+ }
86
+
87
+ // Check if it's followed by terms in parentheses
88
+ if (pos < tokens.length && tokens[pos] === '(') {
89
+ pos++; // Skip opening parenthesis
90
+ var terms = [];
91
+ while (pos < tokens.length && tokens[pos] !== ')') {
92
+ if (tokens[pos] !== ',') {
93
+ terms.push(tokens[pos]);
94
+ }
95
+ pos++;
96
+ }
97
+ if (pos >= tokens.length || tokens[pos] !== ')') {
98
+ throw new Error('Missing closing parenthesis');
99
+ }
100
+ pos++; // Skip closing parenthesis
101
+
102
+ return {
103
+ type: 'atomic',
104
+ predicate: token,
105
+ terms: terms
106
+ };
107
+ }
108
+
109
+ // Single term atomic formula (propositional variable)
110
+ return {
111
+ type: 'atomic',
112
+ predicate: token,
113
+ terms: []
114
+ };
115
+ }
116
+ function parseQuantified() {
117
+ var operator = tokens[pos];
118
+ pos++;
119
+ if (pos >= tokens.length) {
120
+ throw new Error('Incomplete quantified formula');
121
+ }
122
+ var variable = tokens[pos];
123
+ pos++;
124
+ var scope = parseFormula(tokens.slice(pos).join(' '));
125
+ pos = tokens.length;
126
+ return {
127
+ type: 'quantified',
128
+ operator: operator,
129
+ variable: variable,
130
+ scope: scope
131
+ };
132
+ }
133
+ function parseCompound() {
134
+ if (tokens[pos] === '(') {
135
+ pos++; // Skip opening parenthesis
136
+ var formula = parseFormula(tokens.slice(pos).join(' '));
137
+
138
+ // Find matching closing parenthesis
139
+ var parenCount = 1;
140
+ var endPos = pos;
141
+ while (parenCount > 0 && endPos < tokens.length) {
142
+ endPos++;
143
+ if (tokens[endPos] === '(') parenCount++;
144
+ if (tokens[endPos] === ')') parenCount--;
145
+ }
146
+ if (parenCount > 0) {
147
+ throw new Error('Unmatched parentheses');
148
+ }
149
+ pos = endPos + 1;
150
+
151
+ // Check if there's an operator after the parentheses
152
+ if (pos < tokens.length && OPERATOR_SET.has(tokens[pos])) {
153
+ var _operator = tokens[pos];
154
+ pos++;
155
+ var _right = parseFormula(tokens.slice(pos).join(' '));
156
+ pos = tokens.length;
157
+ return {
158
+ type: 'compound',
159
+ operator: _operator,
160
+ left: formula,
161
+ right: _right
162
+ };
163
+ }
164
+ return formula;
165
+ }
166
+ if (tokens[pos] === OPERATORS.NOT) {
167
+ pos++;
168
+ return {
169
+ type: 'compound',
170
+ operator: OPERATORS.NOT,
171
+ right: parseFormula(tokens.slice(pos).join(' '))
172
+ };
173
+ }
174
+ var left = parseAtom();
175
+ if (pos >= tokens.length) {
176
+ return left;
177
+ }
178
+ var operator = tokens[pos];
179
+ if (!OPERATOR_SET.has(operator)) {
180
+ return left;
181
+ }
182
+ pos++;
183
+ var right = parseFormula(tokens.slice(pos).join(' '));
184
+ pos = tokens.length;
185
+ return {
186
+ type: 'compound',
187
+ operator: operator,
188
+ left: left,
189
+ right: right
190
+ };
191
+ }
192
+
193
+ // Start parsing
194
+ if (pos >= tokens.length) {
195
+ throw new Error('Empty formula');
196
+ }
197
+
198
+ // Check for quantifiers
199
+ if (tokens[pos] === OPERATORS.FORALL || tokens[pos] === OPERATORS.EXISTS) {
200
+ return parseQuantified();
201
+ }
202
+
203
+ // Check for identity formulas
204
+ if (tokens.length >= 3 && tokens[1] === OPERATORS.EQUALS) {
205
+ return {
206
+ type: 'identity',
207
+ terms: [tokens[0], tokens[2]]
208
+ };
209
+ }
210
+ return parseCompound();
211
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "RULE_MAP", {
7
+ enumerable: true,
8
+ get: function get() {
9
+ return _types.RULE_MAP;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "getDocumentLineForProofLine", {
13
+ enumerable: true,
14
+ get: function get() {
15
+ return _textParser.getDocumentLineForProofLine;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "getLineForPosition", {
19
+ enumerable: true,
20
+ get: function get() {
21
+ return _textParser.getLineForPosition;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "parseProofText", {
25
+ enumerable: true,
26
+ get: function get() {
27
+ return _textParser.parseProofText;
28
+ }
29
+ });
30
+ var _textParser = require("./textParser.js");
31
+ var _types = require("./types.js");
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getDocumentLineForProofLine = getDocumentLineForProofLine;
7
+ exports.getLineForPosition = getLineForPosition;
8
+ exports.parseProofText = parseProofText;
9
+ var _parser = require("../parser.js");
10
+ var _symbols = require("../symbols.js");
11
+ var _types = require("./types.js");
12
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
13
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
14
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
15
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
16
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
17
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /**
18
+ * Text-format proof parser for Fitch-style natural deduction proofs
19
+ * @module proof/textParser
20
+ */
21
+ /**
22
+ * @typedef {import('./types.js').ProofLine} ProofLine
23
+ * @typedef {import('./types.js').ParsedProof} ParsedProof
24
+ * @typedef {import('./types.js').SubproofCitation} SubproofCitation
25
+ * @typedef {import('../types.js').Formula} Formula
26
+ */
27
+
28
+ /**
29
+ * Parses a text-format Fitch proof into a structured representation
30
+ *
31
+ * @param {string} text - The proof text to parse
32
+ * @returns {ParsedProof} The parsed proof structure
33
+ *
34
+ * @example
35
+ * const proof = parseProofText(`
36
+ * P → Q :PR
37
+ * P :PR
38
+ * Q :→E 1 2
39
+ * // Goal: Q
40
+ * `);
41
+ *
42
+ * @description
43
+ * Text format specification:
44
+ * - Each line: FORMULA :RULE citations
45
+ * - Subproofs indicated by leading pipes: | or ||
46
+ * - Comments: // text
47
+ * - Goal: // Goal: FORMULA
48
+ * - Line citations: space-separated numbers (e.g., 1 2)
49
+ * - Subproof citations: hyphenated ranges (e.g., 3-5)
50
+ */
51
+ function parseProofText(text) {
52
+ /** @type {ProofLine[]} */
53
+ var lines = [];
54
+ var rawLines = text.split('\n');
55
+ var lineNum = 0;
56
+ /** @type {Formula|null} */
57
+ var goal = null;
58
+ /** @type {string|null} */
59
+ var goalText = null;
60
+ /** @type {number[]} */
61
+ var subproofStack = [];
62
+ for (var i = 0; i < rawLines.length; i++) {
63
+ var raw = rawLines[i];
64
+ var trimmed = raw.trim();
65
+
66
+ // Skip empty lines
67
+ if (!trimmed) continue;
68
+
69
+ // Handle comments
70
+ if (trimmed.startsWith('//')) {
71
+ // Check for goal declaration
72
+ var goalMatch = trimmed.match(/\/\/\s*Goal:\s*(.+)/i);
73
+ if (goalMatch) {
74
+ goalText = goalMatch[1].trim();
75
+ try {
76
+ goal = (0, _parser.parseFormula)((0, _symbols.convertAsciiToSymbols)(goalText));
77
+ } catch (_unused) {
78
+ // Invalid goal formula - will be caught during validation
79
+ }
80
+ }
81
+ continue;
82
+ }
83
+ lineNum++;
84
+
85
+ // Calculate depth from leading pipes
86
+ var depthMatch = raw.match(/^(\s*\|*)/);
87
+ var depth = depthMatch ? (depthMatch[1].match(/\|/g) || []).length : 0;
88
+
89
+ // Update subproof stack based on depth changes
90
+ while (subproofStack.length > depth) {
91
+ subproofStack.pop();
92
+ }
93
+ if (depth > subproofStack.length) {
94
+ subproofStack.push(lineNum);
95
+ }
96
+
97
+ // Parse the line content (strip leading pipes and whitespace)
98
+ var content = trimmed.replace(/^\|+\s*/, '');
99
+ var colonIndex = content.lastIndexOf(':');
100
+ var formulaText = '';
101
+ var ruleText = '';
102
+ /** @type {number[]} */
103
+ var citations = [];
104
+ /** @type {SubproofCitation[]} */
105
+ var subproofCitations = [];
106
+ if (colonIndex !== -1) {
107
+ formulaText = content.substring(0, colonIndex).trim();
108
+ var rulePart = content.substring(colonIndex + 1).trim();
109
+ var ruleParts = rulePart.split(/\s+/);
110
+ ruleText = ruleParts[0] || '';
111
+
112
+ // Parse citations
113
+ for (var j = 1; j < ruleParts.length; j++) {
114
+ var cit = ruleParts[j];
115
+ if (cit.includes('-')) {
116
+ var _cit$split$map = cit.split('-').map(Number),
117
+ _cit$split$map2 = _slicedToArray(_cit$split$map, 2),
118
+ start = _cit$split$map2[0],
119
+ end = _cit$split$map2[1];
120
+ if (!isNaN(start) && !isNaN(end)) {
121
+ subproofCitations.push({
122
+ start: start,
123
+ end: end
124
+ });
125
+ }
126
+ } else {
127
+ var num = parseInt(cit, 10);
128
+ if (!isNaN(num)) {
129
+ citations.push(num);
130
+ }
131
+ }
132
+ }
133
+ } else {
134
+ formulaText = content;
135
+ }
136
+
137
+ // Parse formula
138
+ /** @type {Formula|null} */
139
+ var formula = null;
140
+ /** @type {string[]} */
141
+ var issues = [];
142
+ try {
143
+ if (formulaText) {
144
+ formula = (0, _parser.parseFormula)((0, _symbols.convertAsciiToSymbols)(formulaText));
145
+ }
146
+ } catch (e) {
147
+ issues.push("Parse error: ".concat(e instanceof Error ? e.message : 'Invalid formula'));
148
+ }
149
+
150
+ // Normalize rule - try both the raw rule and the converted version
151
+ var convertedRule = (0, _symbols.convertAsciiToSymbols)(ruleText);
152
+ var normalizedRule = _types.RULE_MAP[convertedRule] || _types.RULE_MAP[ruleText] || ruleText;
153
+
154
+ // Validate that rule is known
155
+ if (ruleText && !_types.RULE_MAP[convertedRule] && !_types.RULE_MAP[ruleText]) {
156
+ issues.push("Unknown rule: ".concat(ruleText));
157
+ }
158
+ lines.push({
159
+ lineNum: lineNum,
160
+ rawText: raw,
161
+ formula: formula,
162
+ rule: normalizedRule,
163
+ citations: citations,
164
+ subproofCitations: subproofCitations,
165
+ depth: depth,
166
+ location: [].concat(subproofStack),
167
+ issues: issues
168
+ });
169
+ }
170
+ return {
171
+ lines: lines,
172
+ goal: goal,
173
+ goalText: goalText
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Finds the proof line corresponding to a document position
179
+ *
180
+ * @param {ParsedProof} parsed - The parsed proof
181
+ * @param {string} text - The original document text
182
+ * @param {number} line - The 0-indexed document line number
183
+ * @returns {ProofLine|null} The proof line at that position, or null
184
+ */
185
+ function getLineForPosition(parsed, text, line) {
186
+ var textLines = text.split('\n');
187
+ var proofLineCount = 0;
188
+ for (var i = 0; i < textLines.length && i <= line; i++) {
189
+ var trimmed = textLines[i].trim();
190
+ if (trimmed && !trimmed.startsWith('//')) {
191
+ proofLineCount++;
192
+ if (i === line) {
193
+ return parsed.lines.find(function (l) {
194
+ return l.lineNum === proofLineCount;
195
+ }) || null;
196
+ }
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Finds the document line number for a given proof line number
204
+ *
205
+ * @param {string} text - The original document text
206
+ * @param {number} proofLineNum - The 1-indexed proof line number
207
+ * @returns {number} The 0-indexed document line number
208
+ */
209
+ function getDocumentLineForProofLine(text, proofLineNum) {
210
+ var textLines = text.split('\n');
211
+ var proofLineCount = 0;
212
+ for (var i = 0; i < textLines.length; i++) {
213
+ var trimmed = textLines[i].trim();
214
+ if (trimmed && !trimmed.startsWith('//')) {
215
+ proofLineCount++;
216
+ if (proofLineCount === proofLineNum) {
217
+ return i;
218
+ }
219
+ }
220
+ }
221
+ return 0;
222
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports["default"] = exports.RULE_MAP = void 0;
7
+ /**
8
+ * Type definitions for proof text parsing
9
+ * @module proof/types
10
+ */
11
+
12
+ /**
13
+ * @typedef {import('../types.js').Formula} Formula
14
+ */
15
+
16
+ /**
17
+ * A parsed line from a text-format proof
18
+ * @typedef {Object} ProofLine
19
+ * @property {number} lineNum - The proof line number (1-indexed, excludes comments)
20
+ * @property {string} rawText - The original text of the line
21
+ * @property {Formula|null} formula - The parsed formula, or null if parsing failed
22
+ * @property {string} rule - The normalized rule name (e.g., '∧I', 'Pr')
23
+ * @property {number[]} citations - Line numbers cited in justification
24
+ * @property {SubproofCitation[]} subproofCitations - Subproof ranges cited
25
+ * @property {number} depth - Subproof nesting depth (0 = main proof)
26
+ * @property {number[]} location - Path through subproof tree
27
+ * @property {string[]} issues - Parse errors for this line
28
+ */
29
+
30
+ /**
31
+ * A subproof citation range
32
+ * @typedef {Object} SubproofCitation
33
+ * @property {number} start - Starting line number
34
+ * @property {number} end - Ending line number
35
+ */
36
+
37
+ /**
38
+ * Result of parsing a proof text file
39
+ * @typedef {Object} ParsedProof
40
+ * @property {ProofLine[]} lines - All parsed proof lines
41
+ * @property {Formula|null} goal - The goal formula (from // Goal: comment)
42
+ * @property {string|null} goalText - The raw goal text
43
+ */
44
+
45
+ /**
46
+ * Rule normalization map - maps various ASCII/Unicode rule names to canonical form
47
+ * @type {Record<string, string>}
48
+ */
49
+ var RULE_MAP = exports.RULE_MAP = {
50
+ // Structural rules
51
+ 'PR': 'Pr',
52
+ 'Pr': 'Pr',
53
+ 'Hyp': 'Hyp',
54
+ 'AS': 'Hyp',
55
+ 'R': 'R',
56
+ // Conjunction
57
+ '∧I': '∧I',
58
+ '&I': '∧I',
59
+ '/\\I': '∧I',
60
+ '^I': '∧I',
61
+ '∧E': '∧E',
62
+ '&E': '∧E',
63
+ '/\\E': '∧E',
64
+ '^E': '∧E',
65
+ // Disjunction
66
+ '∨I': '∨I',
67
+ '|I': '∨I',
68
+ '\\/I': '∨I',
69
+ '∨E': '∨E',
70
+ '|E': '∨E',
71
+ '\\/E': '∨E',
72
+ // Implication
73
+ '→I': '→I',
74
+ '->I': '→I',
75
+ '→E': '→E',
76
+ '->E': '→E',
77
+ 'MP': '→E',
78
+ // Biconditional
79
+ '↔I': '↔I',
80
+ '<->I': '↔I',
81
+ '↔E': '↔E',
82
+ '<->E': '↔E',
83
+ // Negation
84
+ '¬I': '¬I',
85
+ '~I': '¬I',
86
+ '!I': '¬I',
87
+ '¬E': '¬E',
88
+ '~E': '¬E',
89
+ '!E': '¬E',
90
+ 'DNE': '¬E',
91
+ // Contradiction
92
+ '⊥I': '⊥I',
93
+ '#I': '⊥I',
94
+ '_|_I': '⊥I',
95
+ '⊥E': '⊥E',
96
+ '#E': '⊥E',
97
+ '_|_E': '⊥E',
98
+ 'X': '⊥E',
99
+ // Quantifiers
100
+ '∀I': '∀I',
101
+ '@I': '∀I',
102
+ '∀E': '∀E',
103
+ '@E': '∀E',
104
+ '∃I': '∃I',
105
+ '$I': '∃I',
106
+ '∃E': '∃E',
107
+ '$E': '∃E',
108
+ // Identity
109
+ '=I': '=I',
110
+ '=E': '=E',
111
+ // Meta rules
112
+ 'LEM': 'LEM',
113
+ 'CQ': 'CQ'
114
+ };
115
+
116
+ // Export empty object to make this a module (types are in JSDoc)
117
+ var _default = exports["default"] = {};