agency-lang 0.0.91 → 0.0.92

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.
@@ -793,13 +793,16 @@ export class AgencyGenerator {
793
793
  return assigned ? multiLine : this.indentStr(multiLine);
794
794
  }
795
795
  processAccessChainElement(node) {
796
+ const dot = node.optional ? "?." : ".";
796
797
  switch (node.kind) {
797
798
  case "property":
798
- return `.${node.name}`;
799
- case "index":
800
- return `[${this.processNode(node.index).trim()}]`;
799
+ return `${dot}${node.name}`;
800
+ case "index": {
801
+ const inner = this.processNode(node.index).trim();
802
+ return node.optional ? `?.[${inner}]` : `[${inner}]`;
803
+ }
801
804
  case "methodCall":
802
- return `.${this.generateFunctionCallExpression(node.functionCall, "valueAccess")}`;
805
+ return `${dot}${this.generateFunctionCallExpression(node.functionCall, "valueAccess")}`;
803
806
  default:
804
807
  throw new Error(`Unknown access chain element kind: ${node.kind}`);
805
808
  }
@@ -661,10 +661,10 @@ export class TypeScriptBuilder {
661
661
  for (const element of node.chain) {
662
662
  switch (element.kind) {
663
663
  case "property":
664
- result = ts.prop(result, element.name);
664
+ result = ts.prop(result, element.name, { optional: element.optional });
665
665
  break;
666
666
  case "index":
667
- result = ts.index(result, this.processNode(element.index));
667
+ result = ts.index(result, this.processNode(element.index), { optional: element.optional });
668
668
  break;
669
669
  case "methodCall": {
670
670
  const callNode = this.generateFunctionCallExpression(element.functionCall, "valueAccess");
@@ -675,15 +675,14 @@ export class TypeScriptBuilder {
675
675
  const args = isClassMethod
676
676
  ? [...callNode.arguments, this.buildMethodCallConfig()]
677
677
  : callNode.arguments;
678
- const callExpr = $(result)
679
- .prop(callNode.callee.name)
680
- .call(args)
681
- .done();
678
+ const propNode = ts.prop(result, callNode.callee.name, { optional: element.optional });
679
+ const callExpr = ts.call(propNode, args);
682
680
  result = isClassMethod ? ts.await(callExpr) : callExpr;
683
681
  }
684
682
  else {
685
683
  // Fallback for complex cases (e.g. await-wrapped)
686
- result = ts.raw(`${this.str(result)}.${this.str(callNode)}`);
684
+ const dot = element.optional ? "?." : ".";
685
+ result = ts.raw(`${this.str(result)}${dot}${this.str(callNode)}`);
687
686
  }
688
687
  break;
689
688
  }
@@ -51,8 +51,12 @@ export declare const ts: {
51
51
  parenLeft?: boolean;
52
52
  parenRight?: boolean;
53
53
  }): TsBinOp;
54
- prop(object: TsNode, property: string): TsPropertyAccess;
55
- index(object: TsNode, property: TsNode): TsPropertyAccess;
54
+ prop(object: TsNode, property: string, opts?: {
55
+ optional?: boolean;
56
+ }): TsPropertyAccess;
57
+ index(object: TsNode, property: TsNode, opts?: {
58
+ optional?: boolean;
59
+ }): TsPropertyAccess;
56
60
  spread(expr: TsNode): TsSpread;
57
61
  id(name: string): TsIdentifier;
58
62
  str(value: string): TsStringLiteral;
@@ -166,11 +166,11 @@ export const ts = {
166
166
  parenRight: opts?.parenRight,
167
167
  };
168
168
  },
169
- prop(object, property) {
170
- return { kind: "propertyAccess", object, property, computed: false };
169
+ prop(object, property, opts) {
170
+ return { kind: "propertyAccess", object, property, computed: false, ...(opts?.optional && { optional: true }) };
171
171
  },
172
- index(object, property) {
173
- return { kind: "propertyAccess", object, property, computed: true };
172
+ index(object, property, opts) {
173
+ return { kind: "propertyAccess", object, property, computed: true, ...(opts?.optional && { optional: true }) };
174
174
  },
175
175
  spread(expr) {
176
176
  return { kind: "spread", expr };
@@ -9,7 +9,9 @@ export declare class TsChain {
9
9
  readonly node: TsNode;
10
10
  constructor(node: TsNode);
11
11
  /** Property access: .prop("foo") → ts.prop(this, "foo") */
12
- prop(name: string): TsChain;
12
+ prop(name: string, opts?: {
13
+ optional?: boolean;
14
+ }): TsChain;
13
15
  map(fn: TsNode): TsChain;
14
16
  /** Computed index: .index(expr) → ts.index(this, expr) */
15
17
  index(expr: TsNode): TsChain;
@@ -11,8 +11,8 @@ export class TsChain {
11
11
  this.node = node;
12
12
  }
13
13
  /** Property access: .prop("foo") → ts.prop(this, "foo") */
14
- prop(name) {
15
- return new TsChain(ts.prop(this.node, name));
14
+ prop(name, opts) {
15
+ return new TsChain(ts.prop(this.node, name, opts));
16
16
  }
17
17
  map(fn) {
18
18
  return new TsChain(ts.call(ts.prop(this.node, "map"), [fn]));
@@ -189,9 +189,11 @@ export function printTs(node, indent = 0) {
189
189
  case "propertyAccess": {
190
190
  const obj = printTs(node.object, indent);
191
191
  if (node.computed) {
192
- return `${obj}[${printTs(node.property, indent)}]`;
192
+ const prefix = node.optional ? "?." : "";
193
+ return `${obj}${prefix}[${printTs(node.property, indent)}]`;
193
194
  }
194
- return `${obj}.${node.property}`;
195
+ const dot = node.optional ? "?." : ".";
196
+ return `${obj}${dot}${node.property}`;
195
197
  }
196
198
  case "spread":
197
199
  return `...${printTs(node.expr, indent)}`;
@@ -153,6 +153,7 @@ export interface TsPropertyAccess {
153
153
  object: TsNode;
154
154
  property: string | TsNode;
155
155
  computed: boolean;
156
+ optional?: boolean;
156
157
  }
157
158
  export interface TsSpread {
158
159
  kind: "spread";
@@ -340,6 +340,73 @@ describe("valueAccessParser", () => {
340
340
  }
341
341
  });
342
342
  });
343
+ describe("optional chaining", () => {
344
+ it('should parse "foo?.bar" with optional property access', () => {
345
+ const result = valueAccessParser("foo?.bar");
346
+ expect(result.success).toBe(true);
347
+ if (result.success) {
348
+ expect(result.result).toEqualWithoutLoc({
349
+ type: "valueAccess",
350
+ base: { type: "variableName", value: "foo" },
351
+ chain: [{ kind: "property", name: "bar", optional: true }],
352
+ });
353
+ }
354
+ });
355
+ it('should parse "foo?.bar?.baz" with chained optional access', () => {
356
+ const result = valueAccessParser("foo?.bar?.baz");
357
+ expect(result.success).toBe(true);
358
+ if (result.success) {
359
+ expect(result.result).toEqualWithoutLoc({
360
+ type: "valueAccess",
361
+ base: { type: "variableName", value: "foo" },
362
+ chain: [
363
+ { kind: "property", name: "bar", optional: true },
364
+ { kind: "property", name: "baz", optional: true },
365
+ ],
366
+ });
367
+ }
368
+ });
369
+ it('should parse "foo?.bar.baz" mixing optional and regular access', () => {
370
+ const result = valueAccessParser("foo?.bar.baz");
371
+ expect(result.success).toBe(true);
372
+ if (result.success) {
373
+ expect(result.result).toEqualWithoutLoc({
374
+ type: "valueAccess",
375
+ base: { type: "variableName", value: "foo" },
376
+ chain: [
377
+ { kind: "property", name: "bar", optional: true },
378
+ { kind: "property", name: "baz" },
379
+ ],
380
+ });
381
+ }
382
+ });
383
+ it('should parse "arr?.[0]" with optional index access', () => {
384
+ const result = valueAccessParser("arr?.[0]");
385
+ expect(result.success).toBe(true);
386
+ if (result.success) {
387
+ expect(result.result).toEqualWithoutLoc({
388
+ type: "valueAccess",
389
+ base: { type: "variableName", value: "arr" },
390
+ chain: [{ kind: "index", index: { type: "number", value: "0" }, optional: true }],
391
+ });
392
+ }
393
+ });
394
+ it('should parse "obj?.method()" with optional method call', () => {
395
+ const result = valueAccessParser("obj?.method()");
396
+ expect(result.success).toBe(true);
397
+ if (result.success) {
398
+ expect(result.result).toEqualWithoutLoc({
399
+ type: "valueAccess",
400
+ base: { type: "variableName", value: "obj" },
401
+ chain: [{
402
+ kind: "methodCall",
403
+ functionCall: { type: "functionCall", functionName: "method", arguments: [] },
404
+ optional: true,
405
+ }],
406
+ });
407
+ }
408
+ });
409
+ });
343
410
  describe("failure cases", () => {
344
411
  it('should fail to parse ""', () => {
345
412
  expect(valueAccessParser("").success).toBe(false);
@@ -524,6 +524,68 @@ describe("binOpParser", () => {
524
524
  }
525
525
  });
526
526
  });
527
+ // Nullish coalescing operator
528
+ describe("nullish coalescing operator", () => {
529
+ it('should parse "a ?? b"', () => {
530
+ const result = binOpParser("a ?? b");
531
+ expect(result.success).toBe(true);
532
+ if (result.success) {
533
+ expect(result.result).toEqualWithoutLoc({
534
+ type: "binOpExpression",
535
+ operator: "??",
536
+ left: { type: "variableName", value: "a" },
537
+ right: { type: "variableName", value: "b" },
538
+ });
539
+ }
540
+ });
541
+ it('should parse "a ?? b ?? c" as left-associative', () => {
542
+ const result = binOpParser("a ?? b ?? c");
543
+ expect(result.success).toBe(true);
544
+ if (result.success) {
545
+ expect(result.result).toEqualWithoutLoc({
546
+ type: "binOpExpression",
547
+ operator: "??",
548
+ left: {
549
+ type: "binOpExpression",
550
+ operator: "??",
551
+ left: { type: "variableName", value: "a" },
552
+ right: { type: "variableName", value: "b" },
553
+ },
554
+ right: { type: "variableName", value: "c" },
555
+ });
556
+ }
557
+ });
558
+ it('should parse "a ?? 42"', () => {
559
+ const result = binOpParser("a ?? 42");
560
+ expect(result.success).toBe(true);
561
+ if (result.success) {
562
+ expect(result.result).toEqualWithoutLoc({
563
+ type: "binOpExpression",
564
+ operator: "??",
565
+ left: { type: "variableName", value: "a" },
566
+ right: { type: "number", value: "42" },
567
+ });
568
+ }
569
+ });
570
+ it('should parse ?? with lower precedence than comparison', () => {
571
+ const result = binOpParser("a == b ?? c");
572
+ expect(result.success).toBe(true);
573
+ if (result.success) {
574
+ // Should parse as (a == b) ?? c
575
+ expect(result.result).toEqualWithoutLoc({
576
+ type: "binOpExpression",
577
+ operator: "??",
578
+ left: {
579
+ type: "binOpExpression",
580
+ operator: "==",
581
+ left: { type: "variableName", value: "a" },
582
+ right: { type: "variableName", value: "b" },
583
+ },
584
+ right: { type: "variableName", value: "c" },
585
+ });
586
+ }
587
+ });
588
+ });
527
589
  // Failure cases
528
590
  describe("regex match operators", () => {
529
591
  const testCases = [
@@ -409,27 +409,35 @@ export const functionCallParser = label("a function call", _functionCallParser);
409
409
  // =============================================================================
410
410
  // access.ts
411
411
  // =============================================================================
412
- // Parse a single chain element: .method(), .property, or [index]
412
+ // Parse "?." or "." returns true for optional, false for regular
413
+ const dotParser = or(map(str("?."), () => true), map(char("."), () => false));
414
+ // Parse a single chain element: .method(), ?.method(), .property, ?.property, [index], ?.[index]
413
415
  const dotMethodCallParser = (input) => {
414
- // First try: . followed by functionCall (name + parens)
415
- const dotResult = char(".")(input);
416
+ const dotResult = dotParser(input);
416
417
  if (!dotResult.success)
417
418
  return failure("expected dot", input);
418
- const fcResult = _functionCallParser(dotResult.rest);
419
+ const optional = dotResult.result;
420
+ const afterDot = dotResult.rest;
421
+ // First try: functionCall (name + parens)
422
+ const fcResult = _functionCallParser(afterDot);
419
423
  if (fcResult.success) {
420
- return success({ kind: "methodCall", functionCall: fcResult.result }, fcResult.rest);
424
+ return success({ kind: "methodCall", functionCall: fcResult.result, ...(optional && { optional: true }) }, fcResult.rest);
421
425
  }
422
- // Second try: . followed by just a property name
423
- const nameResult = variableNameParser(dotResult.rest);
426
+ // Second try: just a property name
427
+ const nameResult = variableNameParser(afterDot);
424
428
  if (nameResult.success) {
425
- return success({ kind: "property", name: nameResult.result.value }, nameResult.rest);
429
+ return success({ kind: "property", name: nameResult.result.value, ...(optional && { optional: true }) }, nameResult.rest);
426
430
  }
427
431
  return failure("expected property name or method call after dot", input);
428
432
  };
433
+ // Parse "?.[" or "[" — returns true for optional, false for regular
434
+ const bracketParser = or(map(str("?.["), () => true), map(char("["), () => false));
429
435
  const indexChainParser = (input) => {
430
- const parser = seqC(set("kind", "index"), char("["), optionalSpaces, capture(lazy(() => exprParser), "index"), optionalSpaces, char("]"));
436
+ const parser = seqC(capture(bracketParser, "optional"), optionalSpaces, capture(lazy(() => exprParser), "index"), optionalSpaces, char("]"));
431
437
  const result = parser(input);
432
- return result;
438
+ if (!result.success)
439
+ return result;
440
+ return success({ kind: "index", index: result.result.index, ...(result.result.optional && { optional: true }) }, result.rest);
433
441
  };
434
442
  const chainElementParser = or(dotMethodCallParser, indexChainParser);
435
443
  export const _valueAccessParser = (input) => {
@@ -607,8 +615,9 @@ export const exprParser = label("an expression", buildExpressionParser(atom, [
607
615
  [
608
616
  { op: wsOp("&&"), assoc: "left", apply: makeBinOp("&&") },
609
617
  ],
610
- // Precedence 1: logical OR
618
+ // Precedence 1: logical OR, nullish coalescing
611
619
  [
620
+ { op: wsOp("??"), assoc: "left", apply: makeBinOp("??") },
612
621
  { op: wsOp("||"), assoc: "left", apply: makeBinOp("||") },
613
622
  ],
614
623
  // Precedence 0: catch (unwrap Result with fallback)
@@ -3,12 +3,15 @@ import { BaseNode } from "./base.js";
3
3
  export type AccessChainElement = {
4
4
  kind: "property";
5
5
  name: string;
6
+ optional?: boolean;
6
7
  } | {
7
8
  kind: "index";
8
9
  index: Expression;
10
+ optional?: boolean;
9
11
  } | {
10
12
  kind: "methodCall";
11
13
  functionCall: FunctionCall;
14
+ optional?: boolean;
12
15
  };
13
16
  export type ValueAccess = BaseNode & {
14
17
  type: "valueAccess";
@@ -1,7 +1,7 @@
1
1
  import { BaseNode } from "./base.js";
2
2
  import { Expression } from "../types.js";
3
3
  export type BinOpArgument = Expression;
4
- export type Operator = "+" | "-" | "*" | "/" | "%" | "==" | "===" | "!=" | "+=" | "-=" | "*=" | "/=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "!" | "=~" | "!~" | "|>" | "catch";
4
+ export type Operator = "+" | "-" | "*" | "/" | "%" | "==" | "===" | "!=" | "+=" | "-=" | "*=" | "/=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "!" | "=~" | "!~" | "??" | "|>" | "catch";
5
5
  export declare const PRECEDENCE: Record<string, number>;
6
6
  export type BinOpExpression = BaseNode & {
7
7
  type: "binOpExpression";
@@ -1,6 +1,7 @@
1
1
  export const PRECEDENCE = {
2
2
  "|>": -1,
3
3
  "catch": 0,
4
+ "??": 1,
4
5
  "||": 1,
5
6
  "&&": 2,
6
7
  "==": 3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agency-lang",
3
- "version": "0.0.91",
3
+ "version": "0.0.92",
4
4
  "description": "The Agency language",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
package/stdlib/array.js CHANGED
@@ -33,6 +33,9 @@ const __globalCtx = new RuntimeContext({
33
33
  dirname: __dirname
34
34
  });
35
35
  const graph = __globalCtx.graph;
36
+ import { TraceWriter } from "agency-lang/runtime";
37
+ const __traceWriter = new TraceWriter("/Users/adityabhargava/agency-lang/tests/debugger/trace-test.agencytrace", "stdlib/array.agency");
38
+ __globalCtx.traceWriter = __traceWriter;
36
39
  // Path-dependent builtin wrappers
37
40
  export function readSkill({ filepath }) {
38
41
  return _readSkillRaw({ filepath, dirname: __dirname });
package/stdlib/index.js CHANGED
@@ -32,6 +32,9 @@ const __globalCtx = new RuntimeContext({
32
32
  dirname: __dirname
33
33
  });
34
34
  const graph = __globalCtx.graph;
35
+ import { TraceWriter } from "agency-lang/runtime";
36
+ const __traceWriter = new TraceWriter("/Users/adityabhargava/agency-lang/tests/debugger/trace-test.agencytrace", "stdlib/index.agency");
37
+ __globalCtx.traceWriter = __traceWriter;
35
38
  // Path-dependent builtin wrappers
36
39
  export function readSkill({ filepath }) {
37
40
  return _readSkillRaw({ filepath, dirname: __dirname });