can1357/oh-my-pi · error · OmpTypeError

Malformed number literal '${raw}'

Error message

Malformed number literal '${raw}'

What it means

The omptype string-definition tokenizer encountered a sequence starting with a digit or '-' that does not form a valid numeric or bigint literal. A number must round-trip exactly: canonical integer or decimal form, no leading zeros, not -0, and String(Number(raw)) === raw. This prevents silently accepting sloppy numeric syntax (e.g. '1.2.3', '01', '1e3', '1_000') that would otherwise parse to an unintended value.

Source

Thrown at packages/omptype/src/ir.ts:248

			const flags = src.slice(j + 1, end);
			try {
				toks.push({ t: "regex", v: new RegExp(source, flags) });
			} catch {
				throw new OmpTypeError(`invalid regular expression "${src.slice(i, end)}"`);
			}
			i = end;
			continue;
		}
		if ((c >= "0" && c <= "9") || (c === "-" && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
			let j = i + 1;
			while (j < n && /[\w.+-]/.test(src[j])) j++;
			const raw = src.slice(i, j);
			if (/^-?(?:0|[1-9]\d*)n$/.test(raw) && raw !== "-0n") {
				toks.push({ t: "bigint", v: BigInt(raw.slice(0, -1)) });
			} else {
				const valid =
					/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw) && !Object.is(Number(raw), -0) && String(Number(raw)) === raw;
				if (!valid) throw new OmpTypeError(`Malformed number literal '${raw}'`);
				toks.push({ t: "num", v: Number(raw) });
			}
			i = j;
			continue;
		}
		if (/[a-zA-Z_$]/.test(c)) {
			let j = i + 1;
			while (j < n && /[\w.$]/.test(src[j])) j++;
			toks.push({ t: "id", v: src.slice(i, j) });
			i = j;
			continue;
		}
		if (c === "<" || c === ">") {
			if (src[i + 1] === "=") {
				toks.push({ t: "op", v: `${c}=` });
				i += 2;
			} else {
				toks.push({ t: "op", v: c });

View on GitHub (pinned to 9690622007)

Solutions

  1. Rewrite the literal in plain canonical decimal form: '1e5' → '100000', '007' → '7', '0x10' → '16'.
  2. Remove unit suffixes or separators from the numeric portion; express units elsewhere in your schema.
  3. If the value comes from code, pre-format it so String(Number(v)) === v (e.g. use plain integer strings), or pass the value as a runtime default (= literal) instead of embedding it.
  4. If a bigint is intended, use the exact canonical form '5n' (never '-0n', which is explicitly rejected).

Example fix

// before
const def = `number > ${min}`; // min = 1e6 -> "number > 1000000" ok, but min=1e21 -> "number > 1e+21" throws
// after
const def = `number > ${min.toFixed(0)}`; // always plain decimal digits
Defensive patterns

Strategy: validation

Validate before calling

function isValidNumberLiteral(s) {
  return /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(s) && !Object.is(Number(s), -0) && String(Number(s)) === s;
}
if (!isValidNumberLiteral(String(min))) throw new Error(`bad literal: ${min}`);

Try / catch

try {
  const schema = parse(def);
} catch (e) {
  if (String(e.message).startsWith("Malformed number literal")) {
    // fall back to a sanitized literal: String(Number(raw)) or toFixed(0)
  }
  throw e;
}

Prevention

When it happens

Trigger: tokenize() (via StrParser's constructor, i.e. any string-definition parse) hits a digit-run containing exponent notation ('1e5'), leading zeros ('007'), double dots ('1.2.3'), trailing junk ('3px'), underscores ('1_000'), or a bigint spelled as '-0n'.

Common situations: Porting TypeScript type expressions like 'number >= 1e6' or '0x10' into omptype string definitions; copy-pasted config values with units ('30min'); building definitions programmatically with Number formatting that emits exponents ('1e+21' from String(1e21)).

Understand the failure class

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/0499f38ec8f8f16d. Report an issue: GitHub.