@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.
- package/dist/{chunk-3YUQPQPZ.js → chunk-DHNWMOP7.js} +371 -62
- package/dist/chunk-DHNWMOP7.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +76 -21
- package/dist/index.js +1 -1
- package/examples/parametric.arch +31 -0
- package/package.json +1 -1
- package/dist/chunk-3YUQPQPZ.js.map +0 -1
|
@@ -100,19 +100,42 @@ function lex(src) {
|
|
|
100
100
|
push("arrow", "->", startLine, startCol, startIdx);
|
|
101
101
|
continue;
|
|
102
102
|
}
|
|
103
|
-
if (
|
|
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) === "
|
|
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.
|
|
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 ${
|
|
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.
|
|
265
|
-
const points = n.points.map(ctx.snapPt);
|
|
266
|
-
const
|
|
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
|
|
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
|
|
323
|
-
const node = { kind: "room", id, at, size
|
|
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.
|
|
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.
|
|
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.
|
|
402
|
-
const at = ctx.snapPt(n.at);
|
|
403
|
-
const
|
|
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.
|
|
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.
|
|
470
|
-
const at = ctx.snapPt(n.at);
|
|
471
|
-
const
|
|
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
|
|
529
|
-
const node = { kind: "furniture", id, category, at, size
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
664
|
-
return { kind: "column", id, at, size
|
|
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.
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
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.
|
|
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 ${
|
|
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.
|
|
1103
|
+
const x = parseExpr(this.ctx);
|
|
914
1104
|
this.eat("comma");
|
|
915
|
-
const y = this.
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
996
|
-
if (node.kind !== def.kind) continue;
|
|
997
|
-
|
|
998
|
-
|
|
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 =
|
|
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-
|
|
1639
|
+
//# sourceMappingURL=chunk-DHNWMOP7.js.map
|