@chanmeng666/archlang 0.3.0 → 0.4.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.
@@ -100,19 +100,42 @@ function lex(src) {
100
100
  push("arrow", "->", startLine, startCol, startIdx);
101
101
  continue;
102
102
  }
103
- if (isDigit(c) || c === "-" && isDigit(peek(1)) || c === "." && isDigit(peek(1))) {
103
+ if (c === "+") {
104
+ advance();
105
+ push("plus", "+", startLine, startCol, startIdx);
106
+ continue;
107
+ }
108
+ if (c === "-") {
109
+ advance();
110
+ push("minus", "-", startLine, startCol, startIdx);
111
+ continue;
112
+ }
113
+ if (c === "*") {
114
+ advance();
115
+ push("star", "*", startLine, startCol, startIdx);
116
+ continue;
117
+ }
118
+ if (c === "/") {
119
+ advance();
120
+ push("slash", "/", startLine, startCol, startIdx);
121
+ continue;
122
+ }
123
+ if (c === "%") {
124
+ advance();
125
+ push("percent", "%", startLine, startCol, startIdx);
126
+ continue;
127
+ }
128
+ if (isDigit(c) || c === "." && isDigit(peek(1))) {
104
129
  let raw = "";
105
- if (c === "-") raw += advance();
106
130
  while (isDigit(peek())) raw += advance();
107
131
  if (peek() === ".") {
108
132
  raw += advance();
109
133
  while (isDigit(peek())) raw += advance();
110
134
  }
111
135
  const first = parseFloat(raw);
112
- if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "-" && isDigit(peek(2)) || peek(1) === "." && isDigit(peek(2)))) {
136
+ if (peek() === "x" && (isDigit(peek(1)) || peek(1) === "." && isDigit(peek(2)))) {
113
137
  advance();
114
138
  let raw2 = "";
115
- if (peek() === "-") raw2 += advance();
116
139
  while (isDigit(peek())) raw2 += advance();
117
140
  if (peek() === ".") {
118
141
  raw2 += advance();
@@ -138,6 +161,146 @@ function lex(src) {
138
161
  return { tokens, errors };
139
162
  }
140
163
 
164
+ // src/expr.ts
165
+ var BIN_PREC = {
166
+ plus: 1,
167
+ minus: 1,
168
+ star: 2,
169
+ slash: 2,
170
+ percent: 2
171
+ };
172
+ var BIN_OP = {
173
+ plus: "+",
174
+ minus: "-",
175
+ star: "*",
176
+ slash: "/",
177
+ percent: "%"
178
+ };
179
+ function parseExpr(ts) {
180
+ return parseBin(ts, 1);
181
+ }
182
+ function parseBin(ts, minPrec) {
183
+ let left = parseUnary(ts);
184
+ for (; ; ) {
185
+ const t = ts.peek();
186
+ const prec = BIN_PREC[t.type];
187
+ if (prec === void 0 || prec < minPrec) break;
188
+ ts.next();
189
+ const right = parseBin(ts, prec + 1);
190
+ left = { t: "bin", op: BIN_OP[t.type], l: left, r: right };
191
+ }
192
+ return left;
193
+ }
194
+ function parseUnary(ts) {
195
+ const t = ts.peek();
196
+ if (t.type === "minus" || t.type === "plus") {
197
+ ts.next();
198
+ return { t: "unary", op: t.type === "minus" ? "-" : "+", e: parseUnary(ts) };
199
+ }
200
+ return parseAtom(ts);
201
+ }
202
+ function parseAtom(ts) {
203
+ const t = ts.peek();
204
+ if (t.type === "number") {
205
+ ts.next();
206
+ return { t: "num", value: t.num };
207
+ }
208
+ if (t.type === "ident") {
209
+ ts.next();
210
+ return { t: "ref", name: t.value, span: { start: t.start, end: t.end } };
211
+ }
212
+ if (t.type === "lparen") {
213
+ ts.next();
214
+ const e = parseExpr(ts);
215
+ const close = ts.peek();
216
+ if (close.type !== "rparen") ts.fail(`Expected ")" but found ${describe(close)}`);
217
+ ts.next();
218
+ return e;
219
+ }
220
+ return ts.fail(`Expected a number, name, or "(" but found ${describe(t)}`);
221
+ }
222
+ function evalExpr(e, env, onError) {
223
+ switch (e.t) {
224
+ case "num":
225
+ return e.value;
226
+ case "ref": {
227
+ const v = env.get(e.name);
228
+ if (v === void 0) {
229
+ const hint = closest(e.name, [...env.keys()]);
230
+ onError({
231
+ severity: "error",
232
+ message: `Unknown name "${e.name}"`,
233
+ code: "E_UNKNOWN_REF",
234
+ span: e.span,
235
+ hints: hint ? [`did you mean "${hint}"?`] : void 0
236
+ });
237
+ return 0;
238
+ }
239
+ return v;
240
+ }
241
+ case "unary": {
242
+ const v = evalExpr(e.e, env, onError);
243
+ return e.op === "-" ? -v : v;
244
+ }
245
+ case "bin": {
246
+ const l = evalExpr(e.l, env, onError);
247
+ const r = evalExpr(e.r, env, onError);
248
+ switch (e.op) {
249
+ case "+":
250
+ return l + r;
251
+ case "-":
252
+ return l - r;
253
+ case "*":
254
+ return l * r;
255
+ case "/":
256
+ case "%":
257
+ if (r === 0) {
258
+ onError({ severity: "error", message: `${e.op === "/" ? "Division" : "Modulo"} by zero`, code: "E_DIV_ZERO" });
259
+ return 0;
260
+ }
261
+ return e.op === "/" ? l / r : l % r;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ function describe(t) {
267
+ if (t.type === "eof") return "end of input";
268
+ if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
269
+ return `"${t.value}"`;
270
+ }
271
+ function closest(name, candidates) {
272
+ let best = null;
273
+ let bestDist = Infinity;
274
+ for (const c of candidates) {
275
+ const d = levenshtein(name, c);
276
+ if (d < bestDist) {
277
+ bestDist = d;
278
+ best = c;
279
+ }
280
+ }
281
+ const limit = Math.max(2, Math.floor(name.length / 3));
282
+ return best !== null && bestDist <= limit ? best : null;
283
+ }
284
+ function levenshtein(a, b) {
285
+ const m = a.length;
286
+ const n = b.length;
287
+ const dp = Array.from({ length: m + 1 }, (_, i) => i);
288
+ for (let j = 1; j <= n; j++) {
289
+ let prev = dp[0];
290
+ dp[0] = j;
291
+ for (let i = 1; i <= m; i++) {
292
+ const tmp = dp[i];
293
+ dp[i] = Math.min(
294
+ dp[i] + 1,
295
+ dp[i - 1] + 1,
296
+ prev + (a[i - 1] === b[j - 1] ? 0 : 1)
297
+ );
298
+ prev = tmp;
299
+ }
300
+ }
301
+ return dp[m];
302
+ }
303
+
141
304
  // src/geometry.ts
142
305
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
143
306
  var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
@@ -238,7 +401,7 @@ var wall = {
238
401
  const id = ctx.parseIdOpt();
239
402
  const category = ctx.eatIdent().value;
240
403
  ctx.eatKeyword("thickness");
241
- const thickness = ctx.eatNumber();
404
+ const thickness = ctx.parseExpr();
242
405
  ctx.eat("lcurly");
243
406
  const points = [];
244
407
  let closed = false;
@@ -252,7 +415,7 @@ var wall = {
252
415
  points.push(ctx.parsePoint());
253
416
  continue;
254
417
  }
255
- ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe(ctx)}`);
418
+ ctx.fail(`Expected a point "(x,y)" or "close" in wall body but found ${describe2(ctx)}`);
256
419
  }
257
420
  ctx.eat("rcurly");
258
421
  if (points.length < 2) ctx.fail("A wall needs at least two points", kw);
@@ -261,9 +424,10 @@ var wall = {
261
424
  idPrefix: (node) => node.category || "wall",
262
425
  resolve(node, ctx) {
263
426
  const n = node;
264
- const id = ctx.idOf(n);
265
- const points = n.points.map(ctx.snapPt);
266
- const thickness = ctx.snap(n.thickness) || n.thickness;
427
+ const id = ctx.id;
428
+ const points = n.points.map((p) => ctx.snapPt(ctx.evalPt(p)));
429
+ const tv = ctx.eval(n.thickness);
430
+ const thickness = ctx.snap(tv) || tv;
267
431
  if (thickness <= 0) {
268
432
  ctx.diag({ severity: "error", message: `Wall "${id}" must have a positive thickness`, code: "E_WALL_THICKNESS", span: n.span });
269
433
  }
@@ -302,7 +466,7 @@ var wall = {
302
466
  return ops;
303
467
  }
304
468
  };
305
- function describe(ctx) {
469
+ function describe2(ctx) {
306
470
  const t = ctx.peek();
307
471
  if (t.type === "eof") return "end of input";
308
472
  if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
@@ -319,8 +483,8 @@ var room = {
319
483
  ctx.eatKeyword("at");
320
484
  const at = ctx.parsePoint();
321
485
  ctx.eatKeyword("size");
322
- const dim2 = ctx.eat("dimension");
323
- const node = { kind: "room", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
486
+ const size = ctx.parseDimensions();
487
+ const node = { kind: "room", id, at, size, line: kw.line };
324
488
  if (ctx.isKeyword("label")) {
325
489
  ctx.next();
326
490
  node.label = ctx.eatString();
@@ -330,9 +494,9 @@ var room = {
330
494
  idPrefix: () => "room",
331
495
  resolve(node, ctx) {
332
496
  const n = node;
333
- const id = ctx.idOf(n);
334
- const at = ctx.snapPt(n.at);
335
- const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
497
+ const id = ctx.id;
498
+ const at = ctx.snapPt(ctx.evalPt(n.at));
499
+ const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
336
500
  if (size.w <= 0 || size.h <= 0) {
337
501
  ctx.diag({ severity: "error", message: `Room "${id}" must have a positive size`, code: "E_ROOM_SIZE", span: n.span });
338
502
  }
@@ -375,7 +539,7 @@ var door = {
375
539
  ctx.eatKeyword("at");
376
540
  const at = ctx.parsePoint();
377
541
  ctx.eatKeyword("width");
378
- const width = ctx.eatNumber();
542
+ const width = ctx.parseExpr();
379
543
  const node = { kind: "door", id, at, width, hinge: "left", swing: "in", line: kw.line };
380
544
  if (ctx.isKeyword("wall")) {
381
545
  ctx.next();
@@ -398,9 +562,10 @@ var door = {
398
562
  idPrefix: () => "door",
399
563
  resolve(node, ctx) {
400
564
  const n = node;
401
- const id = ctx.idOf(n);
402
- const at = ctx.snapPt(n.at);
403
- const width = ctx.snap(n.width) || n.width;
565
+ const id = ctx.id;
566
+ const at = ctx.snapPt(ctx.evalPt(n.at));
567
+ const wv = ctx.eval(n.width);
568
+ const width = ctx.snap(wv) || wv;
404
569
  if (width <= 0) {
405
570
  ctx.diag({ severity: "error", message: `Door "${id}" must have a positive width`, code: "E_DOOR_WIDTH", span: n.span });
406
571
  }
@@ -455,7 +620,7 @@ var windowEl = {
455
620
  ctx.eatKeyword("at");
456
621
  const at = ctx.parsePoint();
457
622
  ctx.eatKeyword("width");
458
- const width = ctx.eatNumber();
623
+ const width = ctx.parseExpr();
459
624
  const node = { kind: "window", id, at, width, line: kw.line };
460
625
  if (ctx.isKeyword("wall")) {
461
626
  ctx.next();
@@ -466,9 +631,10 @@ var windowEl = {
466
631
  idPrefix: () => "window",
467
632
  resolve(node, ctx) {
468
633
  const n = node;
469
- const id = ctx.idOf(n);
470
- const at = ctx.snapPt(n.at);
471
- const width = ctx.snap(n.width) || n.width;
634
+ const id = ctx.id;
635
+ const at = ctx.snapPt(ctx.evalPt(n.at));
636
+ const wv = ctx.eval(n.width);
637
+ const width = ctx.snap(wv) || wv;
472
638
  if (width <= 0) {
473
639
  ctx.diag({ severity: "error", message: `Window "${id}" must have a positive width`, code: "E_WINDOW_WIDTH", span: n.span });
474
640
  }
@@ -525,8 +691,8 @@ var furniture = {
525
691
  ctx.eatKeyword("at");
526
692
  const at = ctx.parsePoint();
527
693
  ctx.eatKeyword("size");
528
- const dim2 = ctx.eat("dimension");
529
- const node = { kind: "furniture", id, category, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
694
+ const size = ctx.parseDimensions();
695
+ const node = { kind: "furniture", id, category, at, size, line: kw.line };
530
696
  if (ctx.isKeyword("label")) {
531
697
  ctx.next();
532
698
  node.label = ctx.eatString();
@@ -536,9 +702,9 @@ var furniture = {
536
702
  idPrefix: (node) => node.category || "furniture",
537
703
  resolve(node, ctx) {
538
704
  const n = node;
539
- const id = ctx.idOf(n);
540
- const at = ctx.snapPt(n.at);
541
- const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
705
+ const id = ctx.id;
706
+ const at = ctx.snapPt(ctx.evalPt(n.at));
707
+ const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
542
708
  if (size.w <= 0 || size.h <= 0) {
543
709
  ctx.diag({ severity: "error", message: `Furniture "${id}" must have a positive size`, code: "E_FURN_SIZE", span: n.span });
544
710
  }
@@ -578,10 +744,10 @@ var dim = {
578
744
  const from = ctx.parsePoint();
579
745
  ctx.eat("arrow");
580
746
  const to = ctx.parsePoint();
581
- const node = { kind: "dim", id: "", from, to, offset: 300, line: kw.line };
747
+ const node = { kind: "dim", id: "", from, to, offset: { t: "num", value: 300 }, line: kw.line };
582
748
  if (ctx.isKeyword("offset")) {
583
749
  ctx.next();
584
- node.offset = ctx.eatNumber();
750
+ node.offset = ctx.parseExpr();
585
751
  }
586
752
  if (ctx.isKeyword("text")) {
587
753
  ctx.next();
@@ -594,10 +760,10 @@ var dim = {
594
760
  const n = node;
595
761
  return {
596
762
  kind: "dim",
597
- id: ctx.idOf(n),
598
- from: ctx.snapPt(n.from),
599
- to: ctx.snapPt(n.to),
600
- offset: n.offset,
763
+ id: ctx.id,
764
+ from: ctx.snapPt(ctx.evalPt(n.from)),
765
+ to: ctx.snapPt(ctx.evalPt(n.to)),
766
+ offset: ctx.eval(n.offset),
601
767
  text: n.text,
602
768
  span: n.span
603
769
  };
@@ -660,15 +826,15 @@ var column = {
660
826
  ctx.eatKeyword("at");
661
827
  const at = ctx.parsePoint();
662
828
  ctx.eatKeyword("size");
663
- const dim2 = ctx.eat("dimension");
664
- return { kind: "column", id, at, size: { w: dim2.num, h: dim2.num2 }, line: kw.line };
829
+ const size = ctx.parseDimensions();
830
+ return { kind: "column", id, at, size, line: kw.line };
665
831
  },
666
832
  idPrefix: () => "column",
667
833
  resolve(node, ctx) {
668
834
  const n = node;
669
- const id = ctx.idOf(n);
670
- const at = ctx.snapPt(n.at);
671
- const size = { w: ctx.snap(n.size.w), h: ctx.snap(n.size.h) };
835
+ const id = ctx.id;
836
+ const at = ctx.snapPt(ctx.evalPt(n.at));
837
+ const size = { w: ctx.snap(ctx.eval(n.size.w)), h: ctx.snap(ctx.eval(n.size.h)) };
672
838
  if (size.w <= 0 || size.h <= 0) {
673
839
  ctx.diag({ severity: "error", message: `Column "${id}" must have a positive size`, code: "E_COLUMN_SIZE", span: n.span });
674
840
  }
@@ -707,7 +873,7 @@ register(dim);
707
873
  register(column);
708
874
 
709
875
  // src/parser.ts
710
- var SETTINGS = ["units", "grid", "scale", "north", "title"];
876
+ var SETTINGS = ["units", "grid", "scale", "north", "title", "let", "component"];
711
877
  var STATEMENT_STARTS = /* @__PURE__ */ new Set([...SETTINGS, ...registry.keys()]);
712
878
  var ParseError = class extends Error {
713
879
  constructor(message, span) {
@@ -752,6 +918,8 @@ var Parser = class {
752
918
  isKeyword: (kw, o) => this.isKeyword(kw, o),
753
919
  isType: (type) => this.isType(type),
754
920
  parsePoint: () => this.parsePoint(),
921
+ parseExpr: () => parseExpr(this.ctx),
922
+ parseDimensions: () => this.parseDimensions(),
755
923
  parseIdOpt: () => this.parseIdOpt(),
756
924
  fail: (msg, t) => this.fail(msg, t)
757
925
  };
@@ -790,12 +958,12 @@ var Parser = class {
790
958
  }
791
959
  eatKeyword(kw) {
792
960
  const t = this.peek();
793
- if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe2(t)}`);
961
+ if (t.type !== "ident" || t.value !== kw) this.fail(`Expected "${kw}" but found ${describe3(t)}`);
794
962
  return this.next();
795
963
  }
796
964
  eat(type) {
797
965
  const t = this.peek();
798
- if (t.type !== type) this.fail(`Expected ${type} but found ${describe2(t)}`);
966
+ if (t.type !== type) this.fail(`Expected ${type} but found ${describe3(t)}`);
799
967
  return this.next();
800
968
  }
801
969
  eatIdent() {
@@ -817,18 +985,25 @@ var Parser = class {
817
985
  units: "mm",
818
986
  grid: 0,
819
987
  north: "up",
820
- elements: []
988
+ components: /* @__PURE__ */ new Map(),
989
+ body: []
821
990
  };
822
991
  while (!this.isType("rcurly") && !this.isType("eof")) {
823
992
  const t = this.peek();
824
993
  const start = t.start;
825
994
  try {
826
- if (t.type !== "ident") this.fail(`Expected a statement but found ${describe2(t)}`);
995
+ if (t.type !== "ident") this.fail(`Expected a statement but found ${describe3(t)}`);
827
996
  const def = registry.get(t.value);
828
997
  if (def) {
829
998
  const node = def.parse(this.ctx);
830
999
  node.span = this.spanFrom(start);
831
- plan.elements.push(node);
1000
+ plan.body.push(node);
1001
+ continue;
1002
+ }
1003
+ if (plan.components.has(t.value) && this.peek(1).type === "lparen") {
1004
+ const node = this.parseInstance();
1005
+ node.span = this.spanFrom(start);
1006
+ plan.body.push(node);
832
1007
  continue;
833
1008
  }
834
1009
  switch (t.value) {
@@ -861,6 +1036,21 @@ var Parser = class {
861
1036
  plan.title = n;
862
1037
  break;
863
1038
  }
1039
+ case "let": {
1040
+ const n = this.parseLet();
1041
+ n.span = this.spanFrom(start);
1042
+ plan.body.push(n);
1043
+ break;
1044
+ }
1045
+ case "component": {
1046
+ const def2 = this.parseComponent(plan.components);
1047
+ def2.span = this.spanFrom(start);
1048
+ if (plan.components.has(def2.name)) {
1049
+ this.fail(`Component "${def2.name}" is already defined`, t);
1050
+ }
1051
+ plan.components.set(def2.name, def2);
1052
+ break;
1053
+ }
864
1054
  default:
865
1055
  this.fail(`Unknown statement "${t.value}"`, t);
866
1056
  }
@@ -906,23 +1096,35 @@ var Parser = class {
906
1096
  this.next();
907
1097
  return t.value;
908
1098
  }
909
- this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe2(t)}`);
1099
+ this.fail(`Expected a north direction (up|down|left|right|<degrees>) but found ${describe3(t)}`);
910
1100
  }
911
1101
  parsePoint() {
912
1102
  this.eat("lparen");
913
- const x = this.eatNumber();
1103
+ const x = parseExpr(this.ctx);
914
1104
  this.eat("comma");
915
- const y = this.eatNumber();
1105
+ const y = parseExpr(this.ctx);
916
1106
  this.eat("rparen");
917
1107
  return { x, y };
918
1108
  }
1109
+ /** A size: either a `WxH` literal dimension token or `<expr> x <expr>`. */
1110
+ parseDimensions() {
1111
+ if (this.isType("dimension")) {
1112
+ const d = this.eat("dimension");
1113
+ return { w: { t: "num", value: d.num }, h: { t: "num", value: d.num2 } };
1114
+ }
1115
+ const w = parseExpr(this.ctx);
1116
+ if (this.isKeyword("x")) this.next();
1117
+ else this.fail(`Expected "x" between width and height but found ${describe3(this.peek())}`);
1118
+ const h = parseExpr(this.ctx);
1119
+ return { w, h };
1120
+ }
919
1121
  parseTitle() {
920
1122
  const kw = this.eatKeyword("title");
921
1123
  this.eat("lcurly");
922
1124
  const node = { line: kw.line };
923
1125
  while (!this.isType("rcurly") && !this.isType("eof")) {
924
1126
  const t = this.peek();
925
- if (t.type !== "ident") this.fail(`Expected a title field but found ${describe2(t)}`);
1127
+ if (t.type !== "ident") this.fail(`Expected a title field but found ${describe3(t)}`);
926
1128
  switch (t.value) {
927
1129
  case "project":
928
1130
  this.next();
@@ -943,20 +1145,121 @@ var Parser = class {
943
1145
  this.eat("rcurly");
944
1146
  return node;
945
1147
  }
1148
+ parseLet() {
1149
+ const kw = this.eatKeyword("let");
1150
+ const name = this.eatIdent().value;
1151
+ this.eat("equals");
1152
+ const value = parseExpr(this.ctx);
1153
+ return { kind: "let", id: "", name, value, line: kw.line };
1154
+ }
1155
+ parseInstance() {
1156
+ const nameTok = this.eatIdent();
1157
+ this.eat("lparen");
1158
+ const args = [];
1159
+ while (!this.isType("rparen") && !this.isType("eof")) {
1160
+ args.push(parseExpr(this.ctx));
1161
+ if (this.isType("comma")) this.next();
1162
+ else break;
1163
+ }
1164
+ this.eat("rparen");
1165
+ return { kind: "instance", id: "", name: nameTok.value, args, line: nameTok.line };
1166
+ }
1167
+ /** `component NAME(p1, p2, …) { <statements> }`. */
1168
+ parseComponent(components) {
1169
+ const kw = this.eatKeyword("component");
1170
+ const name = this.eatIdent().value;
1171
+ this.eat("lparen");
1172
+ const params = [];
1173
+ while (!this.isType("rparen") && !this.isType("eof")) {
1174
+ params.push(this.eatIdent().value);
1175
+ if (this.isType("comma")) this.next();
1176
+ else break;
1177
+ }
1178
+ this.eat("rparen");
1179
+ this.eat("lcurly");
1180
+ const body = [];
1181
+ while (!this.isType("rcurly") && !this.isType("eof")) {
1182
+ const t = this.peek();
1183
+ const start = t.start;
1184
+ const def = registry.get(t.value);
1185
+ if (def) {
1186
+ const node = def.parse(this.ctx);
1187
+ node.span = this.spanFrom(start);
1188
+ body.push(node);
1189
+ continue;
1190
+ }
1191
+ if (t.value === "let") {
1192
+ const n = this.parseLet();
1193
+ n.span = this.spanFrom(start);
1194
+ body.push(n);
1195
+ continue;
1196
+ }
1197
+ if ((components.has(t.value) || t.value === name) && this.peek(1).type === "lparen") {
1198
+ const n = this.parseInstance();
1199
+ n.span = this.spanFrom(start);
1200
+ body.push(n);
1201
+ continue;
1202
+ }
1203
+ this.fail(`Expected an element, "let", or component call in component body but found ${describe3(t)}`, t);
1204
+ }
1205
+ this.eat("rcurly");
1206
+ return { name, params, body, line: kw.line };
1207
+ }
946
1208
  };
947
- function describe2(t) {
1209
+ function describe3(t) {
948
1210
  if (t.type === "eof") return "end of input";
949
1211
  if (t.type === "string") return `string ${JSON.stringify(t.value)}`;
950
1212
  return `"${t.value}"`;
951
1213
  }
952
1214
 
953
1215
  // src/ir.ts
1216
+ var MAX_DEPTH = 64;
1217
+ function expandScope(body, env, defined, global, components, diagnostics, depth) {
1218
+ const diag = (d) => diagnostics.push(d);
1219
+ const out = [];
1220
+ for (const stmt of body) {
1221
+ if (stmt.kind === "let") {
1222
+ if (defined.has(stmt.name)) {
1223
+ diag({ severity: "error", message: `"${stmt.name}" is already defined in this scope`, code: "E_REDEF", span: stmt.span });
1224
+ continue;
1225
+ }
1226
+ env.set(stmt.name, evalExpr(stmt.value, env, diag));
1227
+ defined.add(stmt.name);
1228
+ } else if (stmt.kind === "instance") {
1229
+ const comp = components.get(stmt.name);
1230
+ if (!comp) {
1231
+ const hint = closest(stmt.name, [...components.keys()]);
1232
+ diag({ severity: "error", message: `Unknown component "${stmt.name}"`, code: "E_UNKNOWN_COMPONENT", span: stmt.span, hints: hint ? [`did you mean "${hint}"?`] : void 0 });
1233
+ continue;
1234
+ }
1235
+ if (depth >= MAX_DEPTH) {
1236
+ diag({ severity: "error", message: `Component recursion too deep (limit ${MAX_DEPTH}) instantiating "${stmt.name}"`, code: "E_RECURSION", span: stmt.span });
1237
+ continue;
1238
+ }
1239
+ if (stmt.args.length !== comp.params.length) {
1240
+ diag({ severity: "error", message: `Component "${stmt.name}" expects ${comp.params.length} argument(s) but got ${stmt.args.length}`, code: "E_ARGCOUNT", span: stmt.span });
1241
+ }
1242
+ const argVals = comp.params.map((_, i) => stmt.args[i] !== void 0 ? evalExpr(stmt.args[i], env, diag) : 0);
1243
+ const childEnv = new Map(global);
1244
+ const childDefined = /* @__PURE__ */ new Set();
1245
+ comp.params.forEach((p, i) => {
1246
+ childEnv.set(p, argVals[i]);
1247
+ childDefined.add(p);
1248
+ });
1249
+ out.push(...expandScope(comp.body, childEnv, childDefined, global, components, diagnostics, depth + 1));
1250
+ } else {
1251
+ out.push({ node: stmt, env: new Map(env), id: "" });
1252
+ }
1253
+ }
1254
+ return out;
1255
+ }
954
1256
  function resolve(ast) {
955
1257
  const diagnostics = [];
956
1258
  const g = ast.grid;
957
1259
  const snap = (v) => g > 0 ? Math.round(v / g) * g : v;
958
1260
  const snapPt = (p) => ({ x: snap(p.x), y: snap(p.y) });
959
- const idMap = /* @__PURE__ */ new Map();
1261
+ const globalEnv = /* @__PURE__ */ new Map();
1262
+ const entries = expandScope(ast.body, globalEnv, /* @__PURE__ */ new Set(), globalEnv, ast.components, diagnostics, 0);
960
1263
  const seen = /* @__PURE__ */ new Set();
961
1264
  const assignId = (provided, prefix, idx, span) => {
962
1265
  if (provided) {
@@ -973,33 +1276,39 @@ function resolve(ast) {
973
1276
  };
974
1277
  for (const def of registryOrder) {
975
1278
  let idx = 0;
976
- for (const node of ast.elements) {
977
- if (node.kind !== def.kind) continue;
1279
+ for (const e of entries) {
1280
+ if (e.node.kind !== def.kind) continue;
978
1281
  idx++;
979
- idMap.set(node, assignId(node.id, def.idPrefix(node), idx, node.span));
1282
+ e.id = assignId(e.node.id, def.idPrefix(e.node), idx, e.node.span);
980
1283
  }
981
1284
  }
982
1285
  const walls = [];
1286
+ let activeEnv = /* @__PURE__ */ new Map();
1287
+ const evalNum = (e) => evalExpr(e, activeEnv, (d) => diagnostics.push(d));
1288
+ const evalPt = (p) => ({ x: evalNum(p.x), y: evalNum(p.y) });
983
1289
  const ctx = {
984
1290
  grid: g,
985
1291
  snap,
986
1292
  snapPt,
987
- idOf: (node) => idMap.get(node) ?? node.id,
1293
+ eval: evalNum,
1294
+ evalPt,
1295
+ id: "",
988
1296
  walls,
989
1297
  hostSegment: (at, ref) => hostSegmentForWalls(walls, at, ref),
990
1298
  isOnWall: (at, ref) => isOnSomeWall(walls, at, ref),
991
1299
  diag: (d) => diagnostics.push(d)
992
1300
  };
993
- const rmap = /* @__PURE__ */ new Map();
994
1301
  for (const def of registryOrder) {
995
- for (const node of ast.elements) {
996
- if (node.kind !== def.kind) continue;
997
- const r = def.resolve(node, ctx);
998
- rmap.set(node, r);
1302
+ for (const e of entries) {
1303
+ if (e.node.kind !== def.kind) continue;
1304
+ activeEnv = e.env;
1305
+ ctx.id = e.id;
1306
+ const r = def.resolve(e.node, ctx);
1307
+ e.resolved = r;
999
1308
  if (r.kind === "wall") walls.push(r);
1000
1309
  }
1001
1310
  }
1002
- const elements = ast.elements.map((n) => rmap.get(n));
1311
+ const elements = entries.map((e) => e.resolved);
1003
1312
  const drawable = elements.some(
1004
1313
  (e) => e.kind === "wall" || e.kind === "room" || e.kind === "furniture" || e.kind === "column"
1005
1314
  );
@@ -1327,4 +1636,4 @@ export {
1327
1636
  compile,
1328
1637
  clearCache
1329
1638
  };
1330
- //# sourceMappingURL=chunk-3YUQPQPZ.js.map
1639
+ //# sourceMappingURL=chunk-DHNWMOP7.js.map