@chanmeng666/archlang 0.1.0 → 0.2.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.
@@ -19,11 +19,12 @@ function lex(src) {
19
19
  }
20
20
  return c;
21
21
  };
22
- const push = (type, value, startLine, startCol, extra) => tokens.push({ type, value, line: startLine, col: startCol, ...extra });
22
+ const push = (type, value, startLine, startCol, startIdx, extra) => tokens.push({ type, value, line: startLine, col: startCol, ...extra, start: startIdx, end: i });
23
23
  while (i < src.length) {
24
24
  const c = peek();
25
25
  const startLine = line;
26
26
  const startCol = col;
27
+ const startIdx = i;
27
28
  if (c === " " || c === " " || c === "\r" || c === "\n") {
28
29
  advance();
29
30
  continue;
@@ -53,50 +54,50 @@ function lex(src) {
53
54
  value += advance();
54
55
  }
55
56
  if (!terminated) {
56
- errors.push({ message: "Unterminated string literal", line: startLine, col: startCol });
57
+ errors.push({ message: "Unterminated string literal", span: { start: startIdx, end: i } });
57
58
  }
58
- push("string", value, startLine, startCol);
59
+ push("string", value, startLine, startCol, startIdx);
59
60
  continue;
60
61
  }
61
62
  if (c === "(") {
62
63
  advance();
63
- push("lparen", "(", startLine, startCol);
64
+ push("lparen", "(", startLine, startCol, startIdx);
64
65
  continue;
65
66
  }
66
67
  if (c === ")") {
67
68
  advance();
68
- push("rparen", ")", startLine, startCol);
69
+ push("rparen", ")", startLine, startCol, startIdx);
69
70
  continue;
70
71
  }
71
72
  if (c === "{") {
72
73
  advance();
73
- push("lcurly", "{", startLine, startCol);
74
+ push("lcurly", "{", startLine, startCol, startIdx);
74
75
  continue;
75
76
  }
76
77
  if (c === "}") {
77
78
  advance();
78
- push("rcurly", "}", startLine, startCol);
79
+ push("rcurly", "}", startLine, startCol, startIdx);
79
80
  continue;
80
81
  }
81
82
  if (c === ",") {
82
83
  advance();
83
- push("comma", ",", startLine, startCol);
84
+ push("comma", ",", startLine, startCol, startIdx);
84
85
  continue;
85
86
  }
86
87
  if (c === "=") {
87
88
  advance();
88
- push("equals", "=", startLine, startCol);
89
+ push("equals", "=", startLine, startCol, startIdx);
89
90
  continue;
90
91
  }
91
92
  if (c === ":") {
92
93
  advance();
93
- push("colon", ":", startLine, startCol);
94
+ push("colon", ":", startLine, startCol, startIdx);
94
95
  continue;
95
96
  }
96
97
  if (c === "-" && peek(1) === ">") {
97
98
  advance();
98
99
  advance();
99
- push("arrow", "->", startLine, startCol);
100
+ push("arrow", "->", startLine, startCol, startIdx);
100
101
  continue;
101
102
  }
102
103
  if (isDigit(c) || c === "-" && isDigit(peek(1)) || c === "." && isDigit(peek(1))) {
@@ -118,52 +119,67 @@ function lex(src) {
118
119
  while (isDigit(peek())) raw2 += advance();
119
120
  }
120
121
  const second = parseFloat(raw2);
121
- push("dimension", `${raw}x${raw2}`, startLine, startCol, { num: first, num2: second });
122
+ push("dimension", `${raw}x${raw2}`, startLine, startCol, startIdx, { num: first, num2: second });
122
123
  continue;
123
124
  }
124
- push("number", raw, startLine, startCol, { num: first });
125
+ push("number", raw, startLine, startCol, startIdx, { num: first });
125
126
  continue;
126
127
  }
127
128
  if (isIdentStart(c)) {
128
129
  let value = "";
129
130
  while (i < src.length && isIdentPart(peek())) value += advance();
130
- push("ident", value, startLine, startCol);
131
+ push("ident", value, startLine, startCol, startIdx);
131
132
  continue;
132
133
  }
133
- errors.push({ message: `Unexpected character ${JSON.stringify(c)}`, line: startLine, col: startCol });
134
+ errors.push({ message: `Unexpected character ${JSON.stringify(c)}`, span: { start: startIdx, end: startIdx + 1 } });
134
135
  advance();
135
136
  }
136
- push("eof", "", line, col);
137
+ push("eof", "", line, col, i);
137
138
  return { tokens, errors };
138
139
  }
139
140
 
140
141
  // src/parser.ts
142
+ var STATEMENT_STARTS = /* @__PURE__ */ new Set([
143
+ "units",
144
+ "grid",
145
+ "scale",
146
+ "north",
147
+ "wall",
148
+ "room",
149
+ "door",
150
+ "window",
151
+ "furniture",
152
+ "dim",
153
+ "title"
154
+ ]);
141
155
  var ParseError = class extends Error {
142
- constructor(message, line, col) {
156
+ constructor(message, span) {
143
157
  super(message);
144
158
  this.message = message;
145
- this.line = line;
146
- this.col = col;
159
+ this.span = span;
147
160
  }
148
161
  message;
149
- line;
150
- col;
162
+ span;
151
163
  };
152
164
  function parse(src) {
153
165
  const { tokens, errors: lexErrors } = lex(src);
154
- if (lexErrors.length > 0) {
155
- return { errors: [lexErrors[0]] };
156
- }
166
+ const lexDiags = lexErrors.map((e) => ({
167
+ severity: "error",
168
+ message: e.message,
169
+ span: e.span
170
+ }));
171
+ const p = new Parser(tokens);
172
+ let plan;
157
173
  try {
158
- const p = new Parser(tokens);
159
- const plan = p.parsePlan();
160
- return { plan, errors: [] };
174
+ plan = p.parsePlan();
161
175
  } catch (e) {
162
176
  if (e instanceof ParseError) {
163
- return { errors: [{ message: e.message, line: e.line, col: e.col }] };
177
+ p.diagnostics.push({ severity: "error", message: e.message, span: e.span });
178
+ } else {
179
+ throw e;
164
180
  }
165
- throw e;
166
181
  }
182
+ return { plan, diagnostics: [...lexDiags, ...p.diagnostics] };
167
183
  }
168
184
  var Parser = class {
169
185
  constructor(toks) {
@@ -171,6 +187,7 @@ var Parser = class {
171
187
  }
172
188
  toks;
173
189
  pos = 0;
190
+ diagnostics = [];
174
191
  peek(o = 0) {
175
192
  return this.toks[Math.min(this.pos + o, this.toks.length - 1)];
176
193
  }
@@ -178,7 +195,21 @@ var Parser = class {
178
195
  return this.toks[Math.min(this.pos++, this.toks.length - 1)];
179
196
  }
180
197
  fail(msg, t = this.peek()) {
181
- throw new ParseError(msg, t.line, t.col);
198
+ throw new ParseError(msg, { start: t.start, end: t.end });
199
+ }
200
+ /** Span from a start offset to the end of the last consumed token. */
201
+ spanFrom(start) {
202
+ const last = this.toks[Math.max(0, Math.min(this.pos - 1, this.toks.length - 1))];
203
+ return { start, end: last.end };
204
+ }
205
+ /** Recover after a statement error: skip to the next statement start or block end. */
206
+ synchronize() {
207
+ if (!this.isType("rcurly") && !this.isType("eof")) this.next();
208
+ while (!this.isType("rcurly") && !this.isType("eof")) {
209
+ const t = this.peek();
210
+ if (t.type === "ident" && STATEMENT_STARTS.has(t.value)) return;
211
+ this.next();
212
+ }
182
213
  }
183
214
  isKeyword(kw, o = 0) {
184
215
  const t = this.peek(o);
@@ -222,57 +253,96 @@ var Parser = class {
222
253
  };
223
254
  while (!this.isType("rcurly") && !this.isType("eof")) {
224
255
  const t = this.peek();
225
- if (t.type !== "ident") this.fail(`Expected a statement but found ${describe(t)}`);
226
- switch (t.value) {
227
- case "units": {
228
- this.next();
229
- const u = this.eatIdent().value;
230
- if (u !== "mm") this.fail(`Unsupported units "${u}" (only "mm" is supported)`, t);
231
- plan.units = "mm";
232
- break;
256
+ const start = t.start;
257
+ try {
258
+ if (t.type !== "ident") this.fail(`Expected a statement but found ${describe(t)}`);
259
+ switch (t.value) {
260
+ case "units": {
261
+ this.next();
262
+ const u = this.eatIdent().value;
263
+ if (u !== "mm") this.fail(`Unsupported units "${u}" (only "mm" is supported)`, t);
264
+ plan.units = "mm";
265
+ break;
266
+ }
267
+ case "grid":
268
+ this.next();
269
+ plan.grid = this.eatNumber();
270
+ break;
271
+ case "scale": {
272
+ this.next();
273
+ const a = this.eatNumber();
274
+ this.eat("colon");
275
+ const b = this.eatNumber();
276
+ plan.scale = `${a}:${b}`;
277
+ break;
278
+ }
279
+ case "north":
280
+ this.next();
281
+ plan.north = this.parseNorth();
282
+ break;
283
+ case "wall": {
284
+ const n = this.parseWall();
285
+ n.span = this.spanFrom(start);
286
+ plan.walls.push(n);
287
+ break;
288
+ }
289
+ case "room": {
290
+ const n = this.parseRoom();
291
+ n.span = this.spanFrom(start);
292
+ plan.rooms.push(n);
293
+ break;
294
+ }
295
+ case "door": {
296
+ const n = this.parseDoor();
297
+ n.span = this.spanFrom(start);
298
+ plan.doors.push(n);
299
+ break;
300
+ }
301
+ case "window": {
302
+ const n = this.parseWindow();
303
+ n.span = this.spanFrom(start);
304
+ plan.windows.push(n);
305
+ break;
306
+ }
307
+ case "furniture": {
308
+ const n = this.parseFurniture();
309
+ n.span = this.spanFrom(start);
310
+ plan.furniture.push(n);
311
+ break;
312
+ }
313
+ case "dim": {
314
+ const n = this.parseDim();
315
+ n.span = this.spanFrom(start);
316
+ plan.dims.push(n);
317
+ break;
318
+ }
319
+ case "title": {
320
+ const n = this.parseTitle();
321
+ n.span = this.spanFrom(start);
322
+ plan.title = n;
323
+ break;
324
+ }
325
+ default:
326
+ this.fail(`Unknown statement "${t.value}"`, t);
233
327
  }
234
- case "grid":
235
- this.next();
236
- plan.grid = this.eatNumber();
237
- break;
238
- case "scale": {
239
- this.next();
240
- const a = this.eatNumber();
241
- this.eat("colon");
242
- const b = this.eatNumber();
243
- plan.scale = `${a}:${b}`;
244
- break;
328
+ } catch (e) {
329
+ if (e instanceof ParseError) {
330
+ this.diagnostics.push({ severity: "error", message: e.message, span: e.span });
331
+ this.synchronize();
332
+ } else {
333
+ throw e;
245
334
  }
246
- case "north":
247
- this.next();
248
- plan.north = this.parseNorth();
249
- break;
250
- case "wall":
251
- plan.walls.push(this.parseWall());
252
- break;
253
- case "room":
254
- plan.rooms.push(this.parseRoom());
255
- break;
256
- case "door":
257
- plan.doors.push(this.parseDoor());
258
- break;
259
- case "window":
260
- plan.windows.push(this.parseWindow());
261
- break;
262
- case "furniture":
263
- plan.furniture.push(this.parseFurniture());
264
- break;
265
- case "dim":
266
- plan.dims.push(this.parseDim());
267
- break;
268
- case "title":
269
- plan.title = this.parseTitle();
270
- break;
271
- default:
272
- this.fail(`Unknown statement "${t.value}"`, t);
273
335
  }
274
336
  }
275
- this.eat("rcurly");
337
+ try {
338
+ this.eat("rcurly");
339
+ } catch (e) {
340
+ if (e instanceof ParseError) {
341
+ this.diagnostics.push({ severity: "error", message: e.message, span: e.span });
342
+ } else {
343
+ throw e;
344
+ }
345
+ }
276
346
  return plan;
277
347
  }
278
348
  isType(type) {
@@ -561,8 +631,9 @@ function planBounds(plan) {
561
631
 
562
632
  // src/validate.ts
563
633
  function validate(plan) {
564
- const errors = [];
565
- const warnings = [];
634
+ const diags = [];
635
+ const error = (message, code, span) => diags.push({ severity: "error", message, code, span });
636
+ const warn = (message, code, span) => diags.push({ severity: "warning", message, code, span });
566
637
  const g = plan.grid;
567
638
  const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
568
639
  const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
@@ -591,10 +662,10 @@ function validate(plan) {
591
662
  dm.to = snapPt(dm.to);
592
663
  }
593
664
  const seen = /* @__PURE__ */ new Set();
594
- const assign = (provided, prefix, idx, line) => {
665
+ const assign = (provided, prefix, idx, span) => {
595
666
  if (provided) {
596
667
  if (seen.has(provided)) {
597
- errors.push({ message: `Duplicate id "${provided}"`, line });
668
+ error(`Duplicate id "${provided}"`, "E_DUP_ID", span);
598
669
  }
599
670
  seen.add(provided);
600
671
  return provided;
@@ -604,32 +675,32 @@ function validate(plan) {
604
675
  seen.add(auto);
605
676
  return auto;
606
677
  };
607
- plan.walls.forEach((w, i) => w.id = assign(w.id, w.kind || "wall", i + 1, w.line));
608
- plan.rooms.forEach((r, i) => r.id = assign(r.id, "room", i + 1, r.line));
609
- plan.doors.forEach((d, i) => d.id = assign(d.id, "door", i + 1, d.line));
610
- plan.windows.forEach((w, i) => w.id = assign(w.id, "window", i + 1, w.line));
611
- plan.furniture.forEach((f, i) => f.id = assign(f.id, f.kind || "furniture", i + 1, f.line));
612
- plan.dims.forEach((d, i) => d.id = assign(d.id, "dim", i + 1, d.line));
678
+ plan.walls.forEach((w, i) => w.id = assign(w.id, w.kind || "wall", i + 1, w.span));
679
+ plan.rooms.forEach((r, i) => r.id = assign(r.id, "room", i + 1, r.span));
680
+ plan.doors.forEach((d, i) => d.id = assign(d.id, "door", i + 1, d.span));
681
+ plan.windows.forEach((w, i) => w.id = assign(w.id, "window", i + 1, w.span));
682
+ plan.furniture.forEach((f, i) => f.id = assign(f.id, f.kind || "furniture", i + 1, f.span));
683
+ plan.dims.forEach((d, i) => d.id = assign(d.id, "dim", i + 1, d.span));
613
684
  for (const r of plan.rooms) {
614
685
  if (r.size.w <= 0 || r.size.h <= 0)
615
- errors.push({ message: `Room "${r.id}" must have a positive size`, line: r.line });
686
+ error(`Room "${r.id}" must have a positive size`, "E_ROOM_SIZE", r.span);
616
687
  }
617
688
  for (const f of plan.furniture) {
618
689
  if (f.size.w <= 0 || f.size.h <= 0)
619
- errors.push({ message: `Furniture "${f.id}" must have a positive size`, line: f.line });
690
+ error(`Furniture "${f.id}" must have a positive size`, "E_FURN_SIZE", f.span);
620
691
  }
621
692
  for (const d of plan.doors) {
622
- if (d.width <= 0) errors.push({ message: `Door "${d.id}" must have a positive width`, line: d.line });
693
+ if (d.width <= 0) error(`Door "${d.id}" must have a positive width`, "E_DOOR_WIDTH", d.span);
623
694
  }
624
695
  for (const w of plan.windows) {
625
- if (w.width <= 0) errors.push({ message: `Window "${w.id}" must have a positive width`, line: w.line });
696
+ if (w.width <= 0) error(`Window "${w.id}" must have a positive width`, "E_WINDOW_WIDTH", w.span);
626
697
  }
627
698
  for (const w of plan.walls) {
628
699
  if (w.thickness <= 0)
629
- errors.push({ message: `Wall "${w.id}" must have a positive thickness`, line: w.line });
700
+ error(`Wall "${w.id}" must have a positive thickness`, "E_WALL_THICKNESS", w.span);
630
701
  }
631
702
  if (plan.walls.length === 0 && plan.rooms.length === 0 && plan.furniture.length === 0) {
632
- warnings.push({ message: "Plan has no walls, rooms, or furniture \u2014 nothing to draw" });
703
+ warn("Plan has no walls, rooms, or furniture \u2014 nothing to draw", "W_EMPTY_PLAN");
633
704
  }
634
705
  const onSomeWall = (at, wallRef) => {
635
706
  const candidates = wallRef ? plan.walls.filter((w) => w.id === wallRef || w.kind === wallRef) : plan.walls;
@@ -646,11 +717,11 @@ function validate(plan) {
646
717
  };
647
718
  for (const d of plan.doors) {
648
719
  if (plan.walls.length > 0 && !onSomeWall(d.at, d.wall))
649
- warnings.push({ message: `Door "${d.id}" does not lie on any wall`, line: d.line });
720
+ warn(`Door "${d.id}" does not lie on any wall`, "W_DOOR_OFF_WALL", d.span);
650
721
  }
651
722
  for (const w of plan.windows) {
652
723
  if (plan.walls.length > 0 && !onSomeWall(w.at, w.wall))
653
- warnings.push({ message: `Window "${w.id}" does not lie on any wall`, line: w.line });
724
+ warn(`Window "${w.id}" does not lie on any wall`, "W_WINDOW_OFF_WALL", w.span);
654
725
  }
655
726
  for (let a = 0; a < plan.rooms.length; a++) {
656
727
  for (let b = a + 1; b < plan.rooms.length; b++) {
@@ -659,11 +730,11 @@ function validate(plan) {
659
730
  const ox = Math.max(0, Math.min(r1.at.x + r1.size.w, r2.at.x + r2.size.w) - Math.max(r1.at.x, r2.at.x));
660
731
  const oy = Math.max(0, Math.min(r1.at.y + r1.size.h, r2.at.y + r2.size.h) - Math.max(r1.at.y, r2.at.y));
661
732
  if (ox > 1 && oy > 1) {
662
- warnings.push({ message: `Rooms "${r1.id}" and "${r2.id}" overlap`, line: r2.line });
733
+ warn(`Rooms "${r1.id}" and "${r2.id}" overlap`, "W_ROOM_OVERLAP", r2.span);
663
734
  }
664
735
  }
665
736
  }
666
- return { errors, warnings };
737
+ return diags;
667
738
  }
668
739
 
669
740
  // src/render.ts
@@ -955,6 +1026,60 @@ function titleBlock(plan, b, margin, refDim, thin) {
955
1026
  return `<g>${parts.join("")}</g>`;
956
1027
  }
957
1028
 
1029
+ // src/diagnostics.ts
1030
+ function offsetToLineCol(source, offset) {
1031
+ const o = Math.max(0, Math.min(offset, source.length));
1032
+ let line = 1;
1033
+ let col = 1;
1034
+ for (let k = 0; k < o; k++) {
1035
+ if (source[k] === "\n") {
1036
+ line++;
1037
+ col = 1;
1038
+ } else {
1039
+ col++;
1040
+ }
1041
+ }
1042
+ return { line, col };
1043
+ }
1044
+ function lineStart(source, offset) {
1045
+ let k = Math.max(0, Math.min(offset, source.length));
1046
+ while (k > 0 && source[k - 1] !== "\n") k--;
1047
+ return k;
1048
+ }
1049
+ function lineEnd(source, offset) {
1050
+ let k = Math.max(0, Math.min(offset, source.length));
1051
+ while (k < source.length && source[k] !== "\n") k++;
1052
+ return k;
1053
+ }
1054
+ function formatDiagnostic(source, d) {
1055
+ const codeTag = d.code ? `[${d.code}]` : "";
1056
+ const header = `${d.severity}${codeTag}: ${d.message}`;
1057
+ const lines = [header];
1058
+ if (d.span) {
1059
+ const { line, col } = offsetToLineCol(source, d.span.start);
1060
+ const ls = lineStart(source, d.span.start);
1061
+ const le = lineEnd(source, d.span.start);
1062
+ const srcLine = source.slice(ls, le);
1063
+ const caretStart = d.span.start - ls;
1064
+ const caretEnd = Math.min(d.span.end, le) - ls;
1065
+ const caretLen = Math.max(1, caretEnd - caretStart);
1066
+ const gutter = String(line);
1067
+ const pad = " ".repeat(gutter.length);
1068
+ lines.push(`${pad} --> ${line}:${col}`);
1069
+ lines.push(`${pad} |`);
1070
+ lines.push(`${gutter} | ${srcLine}`);
1071
+ lines.push(`${pad} | ${" ".repeat(caretStart)}${"^".repeat(caretLen)}`);
1072
+ for (const hint of d.hints ?? []) {
1073
+ lines.push(`${pad} = help: ${hint}`);
1074
+ }
1075
+ } else {
1076
+ for (const hint of d.hints ?? []) {
1077
+ lines.push(` = help: ${hint}`);
1078
+ }
1079
+ }
1080
+ return lines.join("\n");
1081
+ }
1082
+
958
1083
  // src/index.ts
959
1084
  var cache = /* @__PURE__ */ new Map();
960
1085
  var CACHE_MAX = 64;
@@ -974,24 +1099,28 @@ function compile(source, opts = {}) {
974
1099
  }
975
1100
  return result;
976
1101
  }
1102
+ function toLegacy(source, d) {
1103
+ if (!d.span) return { message: d.message };
1104
+ const { line, col } = offsetToLineCol(source, d.span.start);
1105
+ return { message: d.message, line, col };
1106
+ }
977
1107
  function compileUncached(source, opts) {
978
- const { plan, errors: parseErrors } = parse(source);
979
- if (!plan || parseErrors.length > 0) {
980
- return { svg: "", errors: parseErrors, warnings: [] };
981
- }
982
- const { errors, warnings } = validate(plan);
983
- if (errors.length > 0) {
984
- return { svg: "", errors, warnings, ast: plan };
985
- }
986
- const svg = render(plan, opts);
987
- return { svg, errors: [], warnings, ast: plan };
1108
+ const { plan, diagnostics: parseDiags } = parse(source);
1109
+ const diagnostics = plan ? [...parseDiags, ...validate(plan)] : [...parseDiags];
1110
+ const errs = diagnostics.filter((d) => d.severity === "error");
1111
+ const errors = errs.map((d) => toLegacy(source, d));
1112
+ const warnings = diagnostics.filter((d) => d.severity === "warning").map((d) => toLegacy(source, d));
1113
+ const svg = plan && errs.length === 0 ? render(plan, opts) : "";
1114
+ return { svg, errors, warnings, diagnostics, ast: plan };
988
1115
  }
989
1116
  function clearCache() {
990
1117
  cache.clear();
991
1118
  }
992
1119
 
993
1120
  export {
1121
+ offsetToLineCol,
1122
+ formatDiagnostic,
994
1123
  compile,
995
1124
  clearCache
996
1125
  };
997
- //# sourceMappingURL=chunk-J5DEA2KQ.js.map
1126
+ //# sourceMappingURL=chunk-ICYNEDSM.js.map