yukigo 0.1.1 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/analyzer/index.d.ts +1 -1
  3. package/dist/analyzer/index.js +2 -1
  4. package/dist/interpreter/components/PatternMatcher.d.ts +2 -1
  5. package/dist/interpreter/components/RuntimeContext.d.ts +2 -1
  6. package/dist/interpreter/components/RuntimeContext.js +10 -3
  7. package/dist/interpreter/components/TestRunner.js +1 -42
  8. package/dist/interpreter/components/Visitor.d.ts +2 -2
  9. package/dist/interpreter/components/Visitor.js +16 -28
  10. package/dist/interpreter/components/runtimes/FunctionRuntime.d.ts +2 -1
  11. package/dist/interpreter/components/runtimes/FunctionRuntime.js +29 -2
  12. package/dist/interpreter/components/runtimes/LazyRuntime.d.ts +5 -0
  13. package/dist/interpreter/components/runtimes/LazyRuntime.js +70 -39
  14. package/dist/interpreter/entities.d.ts +105 -0
  15. package/dist/interpreter/entities.js +96 -0
  16. package/package.json +2 -2
  17. package/src/analyzer/index.ts +3 -2
  18. package/src/interpreter/components/PatternMatcher.ts +2 -0
  19. package/src/interpreter/components/RuntimeContext.ts +10 -4
  20. package/src/interpreter/components/TestRunner.ts +4 -40
  21. package/src/interpreter/components/Visitor.ts +34 -47
  22. package/src/interpreter/components/runtimes/FunctionRuntime.ts +42 -3
  23. package/src/interpreter/components/runtimes/LazyRuntime.ts +82 -49
  24. package/tests/analyzer/generic.spec.ts +14 -0
  25. package/tests/interpreter/interpreter.spec.ts +10 -0
  26. package/dist/interpreter/components/FunctionRuntime.d.ts +0 -8
  27. package/dist/interpreter/components/FunctionRuntime.js +0 -81
  28. package/dist/interpreter/components/LazyRuntime.d.ts +0 -7
  29. package/dist/interpreter/components/LazyRuntime.js +0 -94
  30. package/dist/interpreter/components/LogicEngine.d.ts +0 -24
  31. package/dist/interpreter/components/LogicEngine.js +0 -173
  32. package/dist/interpreter/components/LogicResolver.d.ts +0 -10
  33. package/dist/interpreter/components/LogicResolver.js +0 -97
  34. package/dist/interpreter/components/ObjectRuntime.d.ts +0 -33
  35. package/dist/interpreter/components/ObjectRuntime.js +0 -123
  36. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,96 @@
1
+ export function isLogicResult(value) {
2
+ return (value &&
3
+ typeof value === "object" &&
4
+ "success" in value &&
5
+ typeof value.success === "boolean" &&
6
+ "solutions" in value &&
7
+ value.solutions instanceof Map);
8
+ }
9
+ export class RuntimePredicate {
10
+ kind;
11
+ identifier;
12
+ equations;
13
+ constructor(kind, identifier, equations) {
14
+ this.kind = kind;
15
+ this.identifier = identifier;
16
+ this.equations = equations;
17
+ }
18
+ pushEquation(eq) {
19
+ this.equations.push(eq);
20
+ }
21
+ }
22
+ // TODO: Replace with correct polymorphism
23
+ export const isRuntimePredicate = (prim) => {
24
+ return prim instanceof RuntimePredicate;
25
+ };
26
+ export class RuntimeEquation {
27
+ patterns;
28
+ body;
29
+ constructor(patterns, body) {
30
+ this.patterns = patterns;
31
+ this.body = body;
32
+ }
33
+ }
34
+ export class RuntimeFunction {
35
+ arity;
36
+ identifier;
37
+ equations;
38
+ pendingArgs;
39
+ closure;
40
+ constructor(arity, identifier, equations, pendingArgs, closure) {
41
+ this.arity = arity;
42
+ this.identifier = identifier;
43
+ this.equations = equations;
44
+ this.pendingArgs = pendingArgs;
45
+ this.closure = closure;
46
+ }
47
+ setPendingArgs(args) {
48
+ this.pendingArgs = args;
49
+ }
50
+ }
51
+ // TODO: Replace with correct polymorphism
52
+ export function isRuntimeFunction(val) {
53
+ return val instanceof RuntimeFunction;
54
+ }
55
+ export class RuntimeClass {
56
+ identifier;
57
+ fields;
58
+ methods;
59
+ mixins;
60
+ superclass;
61
+ constructor(identifier, fields, methods, mixins, superclass) {
62
+ this.identifier = identifier;
63
+ this.fields = fields;
64
+ this.methods = methods;
65
+ this.mixins = mixins;
66
+ this.superclass = superclass;
67
+ }
68
+ }
69
+ // TODO: prev as before
70
+ export function isRuntimeClass(val) {
71
+ return val instanceof RuntimeClass;
72
+ }
73
+ export class RuntimeObject {
74
+ identifier;
75
+ className;
76
+ fields;
77
+ methods;
78
+ constructor(identifier, className, fields, methods) {
79
+ this.identifier = identifier;
80
+ this.className = className;
81
+ this.fields = fields;
82
+ this.methods = methods;
83
+ }
84
+ }
85
+ export function isRuntimeObject(val) {
86
+ return val instanceof RuntimeObject;
87
+ }
88
+ export class LazyList {
89
+ generator;
90
+ constructor(generator) {
91
+ this.generator = generator;
92
+ }
93
+ }
94
+ export function isLazyList(prim) {
95
+ return prim instanceof LazyList;
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yukigo",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  "dependencies": {
16
16
  "typescript": "^5.9.2",
17
17
  "yaml": "^2.8.0",
18
- "yukigo-ast": "0.1.1"
18
+ "yukigo-ast": "^0.2.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/chai": "^5.2.2",
@@ -22,7 +22,7 @@ export type AnalysisResult = {
22
22
  export type InspectionRule = {
23
23
  inspection: string;
24
24
  binding?: string;
25
- args: string[];
25
+ args?: string[];
26
26
  expected: boolean;
27
27
  };
28
28
 
@@ -154,7 +154,8 @@ export class Analyzer {
154
154
  // Execution Loop
155
155
  const isGlobalVisitor = !rule.binding || rule.binding === "*";
156
156
  const normalizedBinding = isGlobalVisitor ? undefined : rule.binding;
157
- const visitor = new InspectionClass(...rule.args, normalizedBinding);
157
+ const args = rule.args ?? []
158
+ const visitor = new InspectionClass(...args, normalizedBinding);
158
159
 
159
160
  for (const { node, binding } of targets) {
160
161
  try {
@@ -21,6 +21,7 @@ import {
21
21
  TypePattern,
22
22
  SimpleType,
23
23
  ListType,
24
+ EnvStack,
24
25
  } from "yukigo-ast";
25
26
  import { Bindings } from "../index.js";
26
27
  import { InterpreterVisitor } from "./Visitor.js";
@@ -61,6 +62,7 @@ export interface InternalConsState {
61
62
  readonly head: PrimitiveValue;
62
63
  readonly tailExpr: Expression;
63
64
  readonly evaluator: ExpressionEvaluator;
65
+ readonly capturedEnv: EnvStack;
64
66
  realizedTail?: PrimitiveValue;
65
67
  }
66
68
 
@@ -4,7 +4,6 @@ import { LazyRuntime } from "./runtimes/LazyRuntime.js";
4
4
  import { ObjectRuntime } from "./runtimes/ObjectRuntime.js";
5
5
  import { createGlobalEnv } from "../utils.js";
6
6
  import { UnboundVariable } from "../errors.js";
7
- import { inspect } from "util";
8
7
 
9
8
  export const DefaultConfiguration: Required<InterpreterConfig> = {
10
9
  lazyLoading: false,
@@ -87,12 +86,12 @@ export class RuntimeContext {
87
86
 
88
87
  return false;
89
88
  }
90
- public popEnv(env: EnvStack) {
91
- if (!env.tail)
89
+ public popEnv() {
90
+ if (!this.env.tail)
92
91
  throw new Error(
93
92
  "Runtime Error: Cannot pop the global environment scope.",
94
93
  );
95
- this.env = env.tail;
94
+ this.env = this.env.tail;
96
95
  }
97
96
  public lookup(name: string): PrimitiveValue {
98
97
  let current: EnvStack | null = this.env;
@@ -110,4 +109,11 @@ export class RuntimeContext {
110
109
  public define(name: string, value: PrimitiveValue): void {
111
110
  this.env.head.set(name, value);
112
111
  }
112
+ public clone(env?: EnvStack): EnvStack {
113
+ const target = env ?? this.env;
114
+ return {
115
+ head: new Map(target.head),
116
+ tail: this.env.tail,
117
+ };
118
+ }
113
119
  }
@@ -2,6 +2,7 @@ import {
2
2
  Assert,
3
3
  Equality,
4
4
  Failure,
5
+ isLazyList,
5
6
  PrimitiveValue,
6
7
  Test,
7
8
  TestGroup,
@@ -84,18 +85,17 @@ class AssertionVisitor extends TraverseVisitor {
84
85
  this.interpreter.evaluate(node.value, (value) => {
85
86
  return () =>
86
87
  this.interpreter.evaluate(node.expected, (expected) => {
87
- return this.isEqual(value, expected, (passed) => {
88
+ this.lazyRuntime.deepEqual(value, expected, (passed) => {
88
89
  if (this.negated === passed) {
89
90
  throw new FailedAssert(
90
- value,
91
- expected,
91
+ value, expected,
92
92
  this.negated
93
93
  ? `Expected ${JSON.stringify(value)} NOT to be equal to ${JSON.stringify(expected)}`
94
94
  : `Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(value)}`,
95
95
  );
96
96
  }
97
97
  return k(undefined);
98
- });
98
+ })
99
99
  });
100
100
  });
101
101
  }
@@ -116,42 +116,6 @@ class AssertionVisitor extends TraverseVisitor {
116
116
  return k(undefined);
117
117
  });
118
118
  }
119
- private isEqual(a: any, b: any, k: Continuation<boolean>): Thunk<boolean> {
120
- if (a === b) return k(true);
121
- if (a && b && typeof a === "object" && typeof b === "object") {
122
- return this.lazyRuntime.realizeList(a, (valA) => {
123
- return () =>
124
- this.lazyRuntime.realizeList(b, (valB) => {
125
- if (Array.isArray(valA) && Array.isArray(valB)) {
126
- if (valA.length !== valB.length) return k(false);
127
- const checkNext = (index: number): Thunk<boolean> => {
128
- if (index >= valA.length) return k(true);
129
- return this.isEqual(valA[index], valB[index], (res) => {
130
- if (!res) return k(false);
131
- return () => checkNext(index + 1);
132
- });
133
- };
134
- return checkNext(0);
135
- }
136
-
137
- if (Array.isArray(valA) !== Array.isArray(valB)) return k(false);
138
-
139
- const keys = Object.keys(valA);
140
- if (keys.length !== Object.keys(valB).length) return k(false);
141
- const checkNextKey = (index: number): Thunk<boolean> => {
142
- if (index >= keys.length) return k(true);
143
- const key = keys[index];
144
- return this.isEqual(valA[key], valB[key], (res) => {
145
- if (!res) return k(false);
146
- return () => checkNextKey(index + 1);
147
- });
148
- };
149
- return checkNextKey(0);
150
- });
151
- });
152
- }
153
- return k(false);
154
- }
155
119
  }
156
120
 
157
121
  export class TestRunner extends TraverseVisitor {
@@ -378,6 +378,17 @@ export class InterpreterVisitor
378
378
  visitComparisonOperation(
379
379
  node: ComparisonOperation,
380
380
  ): CPSThunk<PrimitiveValue> {
381
+ if (node.operator === "Equal" || node.operator === "NotEqual") {
382
+ return (k) =>
383
+ this.evaluate(node.left, (left) => () =>
384
+ this.evaluate(node.right, (right) =>
385
+ this.context.lazyRuntime.deepEqual(left, right, (eq) =>
386
+ k(node.operator === "Equal" ? eq : !eq)
387
+ )
388
+ )
389
+ );
390
+ }
391
+
381
392
  return this.processBinary(
382
393
  node,
383
394
  ComparisonOperationTable,
@@ -684,6 +695,7 @@ export class InterpreterVisitor
684
695
  }
685
696
 
686
697
  visitApplication(node: Application): CPSThunk<PrimitiveValue> {
698
+ const { funcRuntime } = this.context
687
699
  if (this.context.config.debug) {
688
700
  console.log(`[Interpreter] Visiting Application`);
689
701
  }
@@ -695,53 +707,26 @@ export class InterpreterVisitor
695
707
  "Cannot apply non-function",
696
708
  );
697
709
 
698
- return this.evaluate(node.parameter, (arg) => {
699
- const argThunk = () => arg;
700
- const allPendingArgs = func.pendingArgs
701
- ? [...func.pendingArgs, argThunk]
702
- : [argThunk];
703
-
704
- return this.applyArguments(func, allPendingArgs)(k);
705
- });
706
- });
707
- }
708
-
709
- private applyArguments(
710
- func: RuntimeFunction,
711
- args: (PrimitiveValue | (() => PrimitiveValue))[],
712
- ): CPSThunk<PrimitiveValue> {
713
- if (args.length < func.arity) {
714
- return valueToCPS({
715
- ...func,
716
- pendingArgs: args,
717
- });
718
- }
719
-
720
- const argsToConsume = args.slice(0, func.arity);
721
- const remainingArgs = args.slice(func.arity);
722
-
723
- const evaluatedArgs = argsToConsume.map((arg) =>
724
- typeof arg === "function" ? arg() : arg,
725
- );
726
-
727
- if (func.closure) this.context.pushEnv(func.closure.head);
728
- return (cont) => () =>
729
- this.context.funcRuntime.apply(func, evaluatedArgs, (result) => {
730
- if (remainingArgs.length > 0) {
731
- if (isRuntimeFunction(result)) {
732
- const nextArgs = result.pendingArgs
733
- ? [...result.pendingArgs, ...remainingArgs]
734
- : remainingArgs;
735
-
736
- return this.applyArguments(result, nextArgs)(cont);
737
- } else {
738
- throw new InterpreterError(
739
- "Application",
740
- `Too many arguments provided. Result was '${result}' (not a function), but had ${remainingArgs.length} args left.`,
741
- );
742
- }
710
+ const applyFuncToNode = (func: RuntimeFunction): CPSThunk<PrimitiveValue> => (k) =>
711
+ this.evaluate(node.parameter, (arg) => {
712
+ const argThunk = () => arg;
713
+ const allPendingArgs = func.pendingArgs
714
+ ? [...func.pendingArgs, argThunk]
715
+ : [argThunk];
716
+ return funcRuntime.applyArguments(func, allPendingArgs)(k);
717
+ });
718
+ if (func.arity === 0) {
719
+ return funcRuntime.applyArguments(func, [])(resultOfFunc => {
720
+ if (!isRuntimeFunction(resultOfFunc))
721
+ throw new InterpreterError(
722
+ "Application",
723
+ `Cannot apply non-function result of arity-0 function`,
724
+ );
725
+ return applyFuncToNode(resultOfFunc)(k);
726
+ });
743
727
  }
744
- return cont(result);
728
+
729
+ return applyFuncToNode(func)(k);
745
730
  });
746
731
  }
747
732
 
@@ -985,7 +970,9 @@ export class InterpreterVisitor
985
970
  return process(0);
986
971
  };
987
972
  }
988
-
973
+ visitTypeCast(node: TypeCast): CPSThunk<PrimitiveValue> {
974
+ return node.expression.accept(this);
975
+ }
989
976
  visitGenerator(node: YuGenerator): CPSThunk<PrimitiveValue> {
990
977
  return (k) => this.evaluate(node.expression, k);
991
978
  }
@@ -4,16 +4,16 @@ import {
4
4
  UnguardedBody,
5
5
  Sequence,
6
6
  Return,
7
- EnvStack,
8
7
  Function,
9
8
  RuntimeFunction,
9
+ isRuntimeFunction,
10
10
  } from "yukigo-ast";
11
11
  import { Bindings } from "../../index.js";
12
12
  import { PatternMatcher } from "../PatternMatcher.js";
13
13
  import { ExpressionEvaluator } from "../../utils.js";
14
14
  import { InterpreterError } from "../../errors.js";
15
15
  import { EnvBuilderVisitor } from "../EnvBuilder.js";
16
- import { Continuation, Thunk } from "../../trampoline.js";
16
+ import { Continuation, CPSThunk, Thunk, valueToCPS } from "../../trampoline.js";
17
17
  import { RuntimeContext } from "../RuntimeContext.js";
18
18
  import { InterpreterVisitor } from "../Visitor.js";
19
19
 
@@ -35,6 +35,7 @@ export class FunctionRuntime {
35
35
  ): Thunk<PrimitiveValue> {
36
36
  const funcName = func.identifier;
37
37
  const equations = func.equations;
38
+ const oldEnv = this.context.env;
38
39
 
39
40
  if (this.context.config.debug)
40
41
  console.log(
@@ -44,6 +45,7 @@ export class FunctionRuntime {
44
45
 
45
46
  const tryNextEquation = (eqIndex: number): Thunk<PrimitiveValue> => {
46
47
  if (eqIndex >= equations.length) {
48
+ this.context.setEnv(oldEnv);
47
49
  throw new NonExhaustivePatterns(funcName);
48
50
  }
49
51
 
@@ -62,7 +64,6 @@ export class FunctionRuntime {
62
64
  );
63
65
 
64
66
  const localEnv = new Map<string, PrimitiveValue>(bindings);
65
- const oldEnv = this.context.env;
66
67
  if (func.closure) this.context.setEnv(func.closure);
67
68
  this.context.pushEnv(localEnv);
68
69
 
@@ -124,6 +125,44 @@ export class FunctionRuntime {
124
125
  return tryNextEquation(0);
125
126
  }
126
127
 
128
+ public applyArguments(
129
+ func: RuntimeFunction,
130
+ args: (PrimitiveValue | (() => PrimitiveValue))[],
131
+ ): CPSThunk<PrimitiveValue> {
132
+ if (args.length < func.arity) {
133
+ return valueToCPS({
134
+ ...func,
135
+ pendingArgs: args,
136
+ });
137
+ }
138
+
139
+ const argsToConsume = args.slice(0, func.arity);
140
+ const remainingArgs = args.slice(func.arity);
141
+
142
+ const evaluatedArgs = argsToConsume.map((arg) =>
143
+ typeof arg === "function" ? arg() : arg,
144
+ );
145
+
146
+ return (cont) => () =>
147
+ this.apply(func, evaluatedArgs, (result) => {
148
+ if (remainingArgs.length > 0) {
149
+ if (isRuntimeFunction(result)) {
150
+ const nextArgs = result.pendingArgs
151
+ ? [...result.pendingArgs, ...remainingArgs]
152
+ : remainingArgs;
153
+
154
+ return this.applyArguments(result, nextArgs)(cont);
155
+ } else {
156
+ throw new InterpreterError(
157
+ "Application",
158
+ `Too many arguments provided. Result was '${result}' (not a function), but had ${remainingArgs.length} args left.`,
159
+ );
160
+ }
161
+ }
162
+ return cont(result);
163
+ });
164
+ }
165
+
127
166
  private preloadDefinitions(
128
167
  seq: Sequence,
129
168
  evaluatorFactory: EvaluatorFactory,
@@ -118,14 +118,15 @@ export class LazyRuntime {
118
118
  evaluator: ExpressionEvaluator,
119
119
  k: Continuation<PrimitiveValue>,
120
120
  ): Thunk<PrimitiveValue> {
121
- const capturedEnv = this.context.env;
122
121
  const ctx = this.context;
122
+ const capturedEnv = ctx.clone();
123
123
  return evaluator.evaluate(node.head, (head) => {
124
- if (this.context.config.lazyLoading) {
124
+ if (ctx.config.lazyLoading) {
125
125
  const consState: InternalConsState = {
126
126
  head,
127
127
  tailExpr: node.tail,
128
128
  evaluator,
129
+ capturedEnv,
129
130
  realizedTail: undefined,
130
131
  };
131
132
 
@@ -140,13 +141,10 @@ export class LazyRuntime {
140
141
 
141
142
  if (current.realizedTail === undefined) {
142
143
  const prevEnv = ctx.env;
143
- ctx.setEnv(capturedEnv);
144
+ ctx.setEnv(current.capturedEnv);
144
145
  try {
145
146
  current.realizedTail = trampoline(
146
- current.evaluator.evaluate(
147
- current.tailExpr,
148
- idContinuation,
149
- ),
147
+ current.evaluator.evaluate(current.tailExpr, idContinuation),
150
148
  );
151
149
  } finally {
152
150
  ctx.setEnv(prevEnv);
@@ -236,29 +234,34 @@ export class LazyRuntime {
236
234
  evaluator: ExpressionEvaluator,
237
235
  k: Continuation<PrimitiveValue>,
238
236
  ): Thunk<PrimitiveValue> {
239
- const capturedEnv = this.context.env;
240
237
  const ctx = this.context;
241
- return k(
242
- createMemoizedStream(function* () {
243
- const prevEnv = ctx.env;
244
- ctx.setEnv(capturedEnv);
245
- try {
246
- const left = trampoline(evaluator.evaluate(node.left, idContinuation));
238
+
239
+ return evaluator.evaluate(node.left, (left) => {
240
+ const capturedEnv = ctx.clone();
241
+ return k(
242
+ createMemoizedStream(function* () {
247
243
  if (Array.isArray(left)) yield* left;
248
244
  else if (typeof left === "string") yield* left.split("");
249
245
  else if (isLazyList(left)) yield* left.generator();
246
+ else throw new Error("Invalid left operand for lazy Concat");
247
+
248
+ // right evaluates lazily on demand
249
+ const prevEnv = ctx.env;
250
+ ctx.setEnv(capturedEnv);
251
+ let right: PrimitiveValue;
252
+ try {
253
+ right = trampoline(evaluator.evaluate(node.right, idContinuation));
254
+ } finally {
255
+ ctx.setEnv(prevEnv);
256
+ }
250
257
 
251
- const right = trampoline(
252
- evaluator.evaluate(node.right, idContinuation),
253
- );
254
258
  if (Array.isArray(right)) yield* right;
255
259
  else if (typeof right === "string") yield* right.split("");
256
260
  else if (isLazyList(right)) yield* right.generator();
257
- } finally {
258
- ctx.setEnv(prevEnv);
259
- }
260
- }),
261
- );
261
+ else throw new Error("Invalid right operand for lazy Concat");
262
+ }),
263
+ );
264
+ });
262
265
  }
263
266
  public deepEqual<R = boolean>(
264
267
  a: PrimitiveValue,
@@ -267,35 +270,65 @@ export class LazyRuntime {
267
270
  ): Thunk<R> {
268
271
  if (a === b) return k(true);
269
272
 
270
- const compareValues = (valA: any, valB: any): Thunk<R> => {
271
- if (typeof valA === "string" && typeof valB === "string")
272
- return k(valA === valB);
273
-
274
- if (Array.isArray(valA) && Array.isArray(valB)) {
275
- if (valA.length !== valB.length) return k(false);
276
- const compareNext = (index: number): Thunk<R> => {
277
- if (index >= valA.length) return k(true);
278
- return () =>
279
- this.deepEqual(valA[index], valB[index], (isEqual) => {
280
- if (!isEqual) return k(false);
281
- return () => compareNext(index + 1);
282
- });
283
- };
284
- return compareNext(0);
285
- }
286
-
287
- return k(valA == valB);
288
- };
273
+ const aIsListLike = this.isListLike(a);
274
+ const bIsListLike = this.isListLike(b);
275
+ const eitherIsCollection = this.isCollection(a) || this.isCollection(b);
289
276
 
290
- if (isLazyList(a) || isLazyList(b)) {
291
- return this.realizeList(a, (valA) => {
292
- return () =>
293
- this.realizeList(b, (valB) => {
294
- return () => compareValues(valA, valB);
295
- });
296
- });
277
+ if (eitherIsCollection && aIsListLike && bIsListLike) {
278
+ return this.realizeList(a, (valA) => () =>
279
+ this.realizeList(b, (valB) => {
280
+ if (valA.length !== valB.length) return k(false);
281
+ return this.deepEqualCollection(valA, valB, 0, k);
282
+ })
283
+ );
297
284
  }
298
285
 
299
- return compareValues(a, b);
286
+ if (eitherIsCollection) return k(false); // colección vs número → false
287
+
288
+ if (this.isPlainObject(a) && this.isPlainObject(b))
289
+ return this.deepEqualObject(a, b, k);
290
+
291
+ return k(a == b); // primitivos: number, boolean, string==number
292
+ }
293
+ private isPlainObject(val: unknown): val is Record<string, any> {
294
+ return val !== null && typeof val === "object"
295
+ && !isLazyList(val) && !Array.isArray(val);
296
+ }
297
+ private isCollection(val: unknown): val is PrimitiveValue[] | LazyList {
298
+ return Array.isArray(val) || isLazyList(val);
299
+ }
300
+
301
+ private isListLike(val: unknown): val is string | PrimitiveValue[] | LazyList {
302
+ return Array.isArray(val) || isLazyList(val) || typeof val === "string";
303
+ }
304
+ private deepEqualCollection<R>(
305
+ a: PrimitiveValue[],
306
+ b: PrimitiveValue[],
307
+ index: number,
308
+ k: Continuation<boolean, R>,
309
+ ): Thunk<R> {
310
+ if (index >= a.length) return k(true);
311
+ return this.deepEqual(a[index], b[index], (eq) => {
312
+ if (!eq) return k(false);
313
+ return () => this.deepEqualCollection(a, b, index + 1, k);
314
+ });
315
+ }
316
+
317
+ private deepEqualObject<R>(
318
+ a: Record<string, any>,
319
+ b: Record<string, any>,
320
+ k: Continuation<boolean, R>,
321
+ ): Thunk<R> {
322
+ const keys = Object.keys(a);
323
+ if (keys.length !== Object.keys(b).length) return k(false);
324
+ const checkNext = (index: number): Thunk<R> => {
325
+ if (index >= keys.length) return k(true);
326
+ const key = keys[index];
327
+ return this.deepEqual(a[key], b[key], (eq) => {
328
+ if (!eq) return k(false);
329
+ return () => checkNext(index + 1);
330
+ });
331
+ };
332
+ return checkNext(0);
300
333
  }
301
334
  }
@@ -5,6 +5,7 @@ import {
5
5
  ParameterizedType,
6
6
  SymbolPrimitive,
7
7
  SimpleType,
8
+ Function,
8
9
  } from "yukigo-ast";
9
10
 
10
11
  describe("Generic Inspections", () => {
@@ -23,6 +24,19 @@ describe("Generic Inspections", () => {
23
24
  return new TypeSignature(identifier, body);
24
25
  };
25
26
 
27
+ it("should allow expectations without args", () => {
28
+ const ast = [new Function(new SymbolPrimitive("func"), [])];
29
+ const rules: InspectionRule[] = [
30
+ {
31
+ binding: "func",
32
+ inspection: "HasBinding",
33
+ expected: true
34
+ },
35
+ ];
36
+ const analyzer = new Analyzer();
37
+ const results = analyzer.analyze(ast, rules);
38
+ expect(results[0].passed).to.be.true;
39
+ });
26
40
  describe("TypesParameterAs", () => {
27
41
  it("should find a match when parameter type matches at correct index", () => {
28
42
  const ast = [createTypeSignature("f", ["Int", "String"], "Bool")];
@@ -22,9 +22,11 @@ import {
22
22
  RangeExpression,
23
23
  Return,
24
24
  Sequence,
25
+ SimpleType,
25
26
  StringOperation,
26
27
  StringPrimitive,
27
28
  SymbolPrimitive,
29
+ TypeCast,
28
30
  UnguardedBody,
29
31
  Variable,
30
32
  VariablePattern,
@@ -892,6 +894,14 @@ describe("Interpreter Spec", () => {
892
894
  assert.equal(res, 16);
893
895
  });
894
896
  });
897
+ describe("Evaluates TypeCast", () => {
898
+ it("Type casting of number 2 to string should be '2'", () => {
899
+ const res = interpreter.evaluate(
900
+ new TypeCast(new NumberPrimitive(2), new SimpleType("String", [])),
901
+ );
902
+ assert.equal(res, "2");
903
+ });
904
+ });
895
905
  describe("Evaluates Range Expression", () => {
896
906
  it("Evaluates [1..5] to full range", () => {
897
907
  const range = new RangeExpression(