All files / server/services/tutor/curriculum sketch-facts.ts

93.33% Statements 42/45
63.63% Branches 21/33
100% Functions 8/8
94.59% Lines 35/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92                              25x                     25x 25x 25x 25x     760x       1268x         634x 634x 634x 638x 638x     634x 634x 122x 122x 122x 122x 122x 122x 122x 122x 275x       122x 122x     634x 634x 531x 531x 531x     634x                 5943x   2453x   131x   3359x      
import { stripComments } from "@shared/parser-patterns";
import type { FactRequirement } from "./curriculum-schema";
 
export type SketchType = "char" | "byte" | "int" | "long" | "float";
 
export interface SketchFacts {
  readonly typesUsed: readonly SketchType[];
  readonly arrays: readonly { name: string; elementType: SketchType; elementCount: number }[];
  readonly serialCalls: readonly ("print" | "write")[];
}
 
export interface SketchFactExtractor {
  extract(code: string): SketchFacts;
}
 
const TYPE_ALIASES: Readonly<Record<string, SketchType>> = {
  char: "char",
  byte: "byte",
  uint8_t: "byte",
  int: "int",
  "unsigned int": "int",
  long: "long",
  "unsigned long": "long",
  float: "float",
};
 
const TYPE_PATTERN = /\b(?:(?:unsigned|signed)\s+)?(?:char|byte|uint8_t|int|long|float)\b/g;
const ARRAY_PATTERN = /\b(char|byte|uint8_t|int|long|float)\s+([A-Za-z_]\w*)\s*\[([^\]]*)\]/g;
const INITIALIZER_PATTERN = /^\s*=\s*\{([^}]*)\}/;
const SERIAL_PATTERN = /\bSerial\s*\.\s*(print|println|write)\s*\(/gi;
 
function canonicalType(value: string): SketchType | undefined {
  return TYPE_ALIASES[value.trim().toLowerCase()];
}
 
function distinct<T>(values: readonly T[]): readonly T[] {
  return [...new Set(values)];
}
 
export class DefaultSketchFactExtractor implements SketchFactExtractor {
  extract(code: string): SketchFacts {
    const clean = stripComments(code);
    const types: SketchType[] = [];
    for (const match of clean.matchAll(TYPE_PATTERN)) {
      const type = canonicalType(match[0]);
      Eif (type) types.push(type);
    }
 
    const arrays: Array<{ name: string; elementType: SketchType; elementCount: number }> = [];
    for (const match of clean.matchAll(ARRAY_PATTERN)) {
      const elementType = canonicalType(match[1] ?? "");
      const name = match[2];
      Iif (!elementType || !name) continue;
      const initializerMatch = INITIALIZER_PATTERN.exec(clean.slice((match.index ?? 0) + match[0].length));
      const initializer = initializerMatch?.[1]?.trim();
      const declaredSize = Number.parseInt(match[3]?.trim() ?? "", 10);
      let elementCount = 0;
      if (initializer) {
        elementCount = initializer.split(",").map((item) => item.trim()).filter(Boolean).length;
      } else Eif (Number.isSafeInteger(declaredSize) && declaredSize > 0) {
        elementCount = declaredSize;
      }
      arrays.push({ name, elementType, elementCount });
      types.push(elementType);
    }
 
    const serialCalls: ("print" | "write")[] = [];
    for (const match of clean.matchAll(SERIAL_PATTERN)) {
      const operation = match[1]?.toLowerCase();
      if (operation === "print" || operation === "println") serialCalls.push("print");
      if (operation === "write") serialCalls.push("write");
    }
 
    return {
      typesUsed: distinct(types),
      arrays,
      serialCalls: distinct(serialCalls),
    };
  }
}
 
export function matchesFactRequirement(facts: SketchFacts, requirement: FactRequirement): boolean {
  switch (requirement.fact) {
    case "type-used":
      return facts.typesUsed.some((type) => requirement.values?.includes(type) ?? false);
    case "array-declared":
      return facts.arrays.some((array) => requirement.elementTypes?.includes(array.elementType) ?? true);
    case "serial-call":
      return facts.serialCalls.some((operation) => requirement.values?.includes(operation) ?? false);
  }
}