feat(types): Huge + Angle + Percent polish numeric types (#55)
Types phase 5 (polish). A splice-on-demand numeric runtime (runtime/native/numeric.ludic, built on the inline Math.* trig) behind three namespaces: - Huge.* — idle big numbers (normalized mantissa x 10^exponent): from/add/ sub/mul/neg/cmp/sign/mantissa/exp/str (scientific 1.23e45). Display-scale, not lockstep-exact (BigInt/Decimal for exactness). - Angle.* — auto-wrapping radians: from_degrees/to_degrees/wrap/sin/cos/add/ diff (shortest signed rotation)/lerp (shortest arc). - Percent.* — clamped [0,1]: clamp/of/lerp/apply. Remaining phase-5 items are already covered (duration=Duration.*, rune=Unicode.*, i64=long) or need a type-checking pass (handle, typed name, other sized ints) — tracked for later. Wired: parser splice trigger (g_uses_numeric), emit_call dispatch, reseeded seed, a self-asserting example (examples/library/numeric.ludic + feat_case), per-symbol docs + inventory. All suites green incl. golden renders byte- identical and the bootstrap fixpoint. NOTE: fixed `const`s lower to raw-int-typed values (emit_call N_CONST), which breaks fixed comparisons — the runtime uses inline fixed literals instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
539f258d92
commit
5dc8394f22
33 changed files with 23162 additions and 21092 deletions
15
changes/types-huge-angle-percent.md
Normal file
15
changes/types-huge-angle-percent.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**Polish numeric types — `Huge` + `Angle` + `Percent` (#55, types phase 5).**
|
||||
`Huge.*` is an idle/incremental big number (normalized mantissa x 10^exponent)
|
||||
reaching far past the integer range — `from`/`add`/`sub`/`mul`/`neg`/`cmp`/
|
||||
`sign`/`mantissa`/`exp`/`str` (scientific `1.23e45`); it is display-scale, not
|
||||
lockstep-exact (use BigInt/Decimal for exactness). `Angle.*` is an auto-wrapping
|
||||
radian angle — `from_degrees`/`to_degrees`/`wrap`/`sin`/`cos`/`add`/`diff`
|
||||
(shortest signed rotation)/`lerp` (shortest arc) — over the deterministic
|
||||
`Math.*` trig. `Percent.*` is a value clamped to [0,1] — `clamp`/`of`/`lerp`/
|
||||
`apply` — for health fractions, volumes and interpolation `t`. All spliced in on
|
||||
demand. Remaining phase-5 items are already covered or need front-end work:
|
||||
`duration` = `Duration.*`, `rune`/`char` = `Unicode.*`, `i64` = `long`; and the
|
||||
nominal-safety wrappers `handle` / typed `name` plus the other sized ints need a
|
||||
type-checking pass, tracked for later.
|
||||
7
docs/language/angle/_section.md
Normal file
7
docs/language/angle/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: angle
|
||||
title: Angle
|
||||
order: 15
|
||||
---
|
||||
|
||||
An auto-wrapping angle in radians, so you never juggle <code>% TAU</code> by hand. Every <code>Angle.*</code> result is normalized into <code>[-pi, pi)</code>, which makes <code>diff</code> the shortest signed rotation between two headings and <code>lerp</code> turn the short way around. Build from degrees with <code>Angle.from_degrees</code> (read back with <code>to_degrees</code>), take <code>sin</code> / <code>cos</code>, combine with <code>add</code>, and normalize any raw value with <code>wrap</code>. It builds on the deterministic fixed-point <code>Math.*</code> trig, so results are bit-identical on every platform. Arguments are positional; angles are <code>fixed</code> radians. Spliced in only when a program mentions <code>Angle.*</code>.
|
||||
22
docs/language/angle/angle-add.md
Normal file
22
docs/language/angle/angle-add.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-add
|
||||
name: Angle.add
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.add
|
||||
sig: Angle.add(a, b) -> fixed
|
||||
tip: Add two angles (wrapped).
|
||||
order: 5
|
||||
ns: Angle
|
||||
member: add
|
||||
---
|
||||
|
||||
Adds two angles and wraps the result back into range.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
facing = Angle.add(facing, turnRate)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-cos.md
Normal file
22
docs/language/angle/angle-cos.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-cos
|
||||
name: Angle.cos
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.cos
|
||||
sig: Angle.cos(a) -> fixed
|
||||
tip: Cosine of an angle.
|
||||
order: 4
|
||||
ns: Angle
|
||||
member: cos
|
||||
---
|
||||
|
||||
Returns the cosine of the angle, using the deterministic fixed-point trig.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let dx = Angle.cos(facing)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-diff.md
Normal file
22
docs/language/angle/angle-diff.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-diff
|
||||
name: Angle.diff
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.diff
|
||||
sig: Angle.diff(a, b) -> fixed
|
||||
tip: Shortest signed rotation from a to b.
|
||||
order: 6
|
||||
ns: Angle
|
||||
member: diff
|
||||
---
|
||||
|
||||
Returns the shortest signed rotation from <code>a</code> to <code>b</code>, in [-pi, pi) — positive to turn one way, negative the other.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let steer = Angle.diff(facing, target)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-from_degrees.md
Normal file
22
docs/language/angle/angle-from_degrees.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-from_degrees
|
||||
name: Angle.from_degrees
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.from_degrees
|
||||
sig: Angle.from_degrees(d) -> fixed
|
||||
tip: Degrees to a wrapped radian angle.
|
||||
order: 0
|
||||
ns: Angle
|
||||
member: from_degrees
|
||||
---
|
||||
|
||||
Converts degrees to radians and wraps the result into [-pi, pi). The natural way to author a heading a designer types in degrees.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let facing = Angle.from_degrees(90.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-lerp.md
Normal file
22
docs/language/angle/angle-lerp.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-lerp
|
||||
name: Angle.lerp
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.lerp
|
||||
sig: Angle.lerp(a, b, t) -> fixed
|
||||
tip: Interpolate along the shortest arc.
|
||||
order: 7
|
||||
ns: Angle
|
||||
member: lerp
|
||||
---
|
||||
|
||||
Interpolates from <code>a</code> toward <code>b</code> along the shortest arc (t is a fixed 0..1), so a turn never spins the long way around.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
facing = Angle.lerp(facing, target, 0.1)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-sin.md
Normal file
22
docs/language/angle/angle-sin.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-sin
|
||||
name: Angle.sin
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.sin
|
||||
sig: Angle.sin(a) -> fixed
|
||||
tip: Sine of an angle.
|
||||
order: 3
|
||||
ns: Angle
|
||||
member: sin
|
||||
---
|
||||
|
||||
Returns the sine of the angle, using the deterministic fixed-point trig.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let dy = Angle.sin(facing)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-to_degrees.md
Normal file
22
docs/language/angle/angle-to_degrees.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-to_degrees
|
||||
name: Angle.to_degrees
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.to_degrees
|
||||
sig: Angle.to_degrees(a) -> fixed
|
||||
tip: Radians back to degrees.
|
||||
order: 1
|
||||
ns: Angle
|
||||
member: to_degrees
|
||||
---
|
||||
|
||||
Converts a radian angle back to degrees — handy for display or debugging.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let deg = Angle.to_degrees(facing)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/angle/angle-wrap.md
Normal file
22
docs/language/angle/angle-wrap.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: angle-wrap
|
||||
name: Angle.wrap
|
||||
category: angle
|
||||
kind: namespace-method
|
||||
tokens: Angle.wrap
|
||||
sig: Angle.wrap(a) -> fixed
|
||||
tip: Normalize any radian value to [-pi, pi).
|
||||
order: 2
|
||||
ns: Angle
|
||||
member: wrap
|
||||
---
|
||||
|
||||
Normalizes any radian value into [-pi, pi), so accumulated rotation never runs away.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
heading = Angle.wrap(heading + spin)
|
||||
}
|
||||
}
|
||||
```
|
||||
7
docs/language/huge/_section.md
Normal file
7
docs/language/huge/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: huge
|
||||
title: Huge
|
||||
order: 14
|
||||
---
|
||||
|
||||
Idle/incremental big numbers — magnitudes far past what a 32- or 64-bit integer can hold, for prestige currencies and exponential growth. A <code>Huge</code> is a normalized mantissa (a Q16.16 <code>fixed</code> in [1, 10)) times <code>10^exponent</code>, so it can represent 10^100 or 10^1000 while staying one small value. It is a <strong>display-scale</strong> number (about four significant digits), <em>not</em> a lockstep-exact one — keep it out of the deterministic simulation and reach for <code>BigInt</code> / <code>Decimal</code> when exactness matters. Build one with <code>Huge.from</code>, combine with <code>add</code> / <code>sub</code> / <code>mul</code> / <code>neg</code>, compare with <code>cmp</code> / <code>sign</code>, read <code>mantissa</code> / <code>exp</code>, and render as <code>1.23e45</code> with <code>str</code>. Arguments are positional. Spliced in only when a program mentions <code>Huge.*</code>.
|
||||
22
docs/language/huge/huge-add.md
Normal file
22
docs/language/huge/huge-add.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-add
|
||||
name: Huge.add
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.add
|
||||
sig: Huge.add(a, b) -> Huge
|
||||
tip: Sum of two big numbers.
|
||||
order: 1
|
||||
ns: Huge
|
||||
member: add
|
||||
---
|
||||
|
||||
Adds two big numbers, aligning their exponents; a term too small to matter at display precision is dropped.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
gold = Huge.add(gold, income)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-cmp.md
Normal file
22
docs/language/huge/huge-cmp.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-cmp
|
||||
name: Huge.cmp
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.cmp
|
||||
sig: Huge.cmp(a, b) -> int
|
||||
tip: Compare two big numbers: -1, 0, or 1.
|
||||
order: 5
|
||||
ns: Huge
|
||||
member: cmp
|
||||
---
|
||||
|
||||
Compares two big numbers, returning <code>-1</code>, <code>0</code>, or <code>1</code> as <code>a</code> is less than, equal to, or greater than <code>b</code>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
if Huge.cmp(gold, price) >= 0 { buy() }
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-exp.md
Normal file
22
docs/language/huge/huge-exp.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-exp
|
||||
name: Huge.exp
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.exp
|
||||
sig: Huge.exp(a) -> int
|
||||
tip: The base-10 exponent.
|
||||
order: 8
|
||||
ns: Huge
|
||||
member: exp
|
||||
---
|
||||
|
||||
Returns the base-10 exponent — how many orders of magnitude the value spans.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let order = Huge.exp(gold)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-from.md
Normal file
22
docs/language/huge/huge-from.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-from
|
||||
name: Huge.from
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.from
|
||||
sig: Huge.from(value) -> Huge
|
||||
tip: Turn an int into a Huge.
|
||||
order: 0
|
||||
ns: Huge
|
||||
member: from
|
||||
---
|
||||
|
||||
Lifts a plain <code>int</code> into a <code>Huge</code>, the starting point for idle-game magnitudes that grow past the integer range.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
var gold = Huge.from(1000)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-mantissa.md
Normal file
22
docs/language/huge/huge-mantissa.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-mantissa
|
||||
name: Huge.mantissa
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.mantissa
|
||||
sig: Huge.mantissa(a) -> fixed
|
||||
tip: The mantissa in [1, 10).
|
||||
order: 7
|
||||
ns: Huge
|
||||
member: mantissa
|
||||
---
|
||||
|
||||
Returns the normalized mantissa, a <code>fixed</code> in [1, 10) (or 0). Pair it with <code>Huge.exp</code> to format a value your own way.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let m = Huge.mantissa(gold)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-mul.md
Normal file
22
docs/language/huge/huge-mul.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-mul
|
||||
name: Huge.mul
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.mul
|
||||
sig: Huge.mul(a, b) -> Huge
|
||||
tip: Product of two big numbers.
|
||||
order: 3
|
||||
ns: Huge
|
||||
member: mul
|
||||
---
|
||||
|
||||
Multiplies two big numbers (mantissas multiply, exponents add) — the exponential growth step at the heart of an idle game.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
gold = Huge.mul(gold, multiplier)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-neg.md
Normal file
22
docs/language/huge/huge-neg.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-neg
|
||||
name: Huge.neg
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.neg
|
||||
sig: Huge.neg(a) -> Huge
|
||||
tip: Negate a big number.
|
||||
order: 4
|
||||
ns: Huge
|
||||
member: neg
|
||||
---
|
||||
|
||||
Returns <code>-a</code>, the same magnitude with the opposite sign.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let owed = Huge.neg(balance)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-sign.md
Normal file
22
docs/language/huge/huge-sign.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-sign
|
||||
name: Huge.sign
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.sign
|
||||
sig: Huge.sign(a) -> int
|
||||
tip: The sign: -1, 0, or 1.
|
||||
order: 6
|
||||
ns: Huge
|
||||
member: sign
|
||||
---
|
||||
|
||||
Returns the sign of the value: <code>-1</code>, <code>0</code>, or <code>1</code>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
if Huge.sign(balance) < 0 { warn() }
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-str.md
Normal file
22
docs/language/huge/huge-str.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-str
|
||||
name: Huge.str
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.str
|
||||
sig: Huge.str(a) -> string
|
||||
tip: Render as scientific text like 1.23e45.
|
||||
order: 9
|
||||
ns: Huge
|
||||
member: str
|
||||
---
|
||||
|
||||
Renders the value in scientific form, like <code>1.23e45</code> (a two-decimal mantissa). This is how you show an idle magnitude on the HUD.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
Screen.draw_text(8, 8, Huge.str(gold), Color.White, 1)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/huge/huge-sub.md
Normal file
22
docs/language/huge/huge-sub.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: huge-sub
|
||||
name: Huge.sub
|
||||
category: huge
|
||||
kind: namespace-method
|
||||
tokens: Huge.sub
|
||||
sig: Huge.sub(a, b) -> Huge
|
||||
tip: Difference of two big numbers.
|
||||
order: 2
|
||||
ns: Huge
|
||||
member: sub
|
||||
---
|
||||
|
||||
Subtracts <code>b</code> from <code>a</code>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
gold = Huge.sub(gold, cost)
|
||||
}
|
||||
}
|
||||
```
|
||||
7
docs/language/percent/_section.md
Normal file
7
docs/language/percent/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: percent
|
||||
title: Percent
|
||||
order: 16
|
||||
---
|
||||
|
||||
A value clamped to <code>[0, 1]</code> — health fractions, volumes, and the <code>t</code> of an interpolation, without stray values slipping below 0 or above 1. <code>Percent.clamp</code> pins any <code>fixed</code> into range; <code>Percent.of</code> forms a clamped ratio <code>num / den</code>; <code>Percent.lerp</code> interpolates <code>a..b</code> by a clamped <code>t</code>; and <code>Percent.apply</code> scales a value by a clamped fraction. Every operation is deterministic fixed-point. Arguments are positional. Spliced in only when a program mentions <code>Percent.*</code>.
|
||||
22
docs/language/percent/percent-apply.md
Normal file
22
docs/language/percent/percent-apply.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: percent-apply
|
||||
name: Percent.apply
|
||||
category: percent
|
||||
kind: namespace-method
|
||||
tokens: Percent.apply
|
||||
sig: Percent.apply(value, p) -> fixed
|
||||
tip: Scale a value by a clamped percent.
|
||||
order: 3
|
||||
ns: Percent
|
||||
member: apply
|
||||
---
|
||||
|
||||
Scales <code>value</code> by a clamped fraction <code>p</code> — for example, applying a damage-reduction percentage.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let dealt = Percent.apply(damage, 0.75)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/percent/percent-clamp.md
Normal file
22
docs/language/percent/percent-clamp.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: percent-clamp
|
||||
name: Percent.clamp
|
||||
category: percent
|
||||
kind: namespace-method
|
||||
tokens: Percent.clamp
|
||||
sig: Percent.clamp(v) -> fixed
|
||||
tip: Clamp any fixed into [0, 1].
|
||||
order: 0
|
||||
ns: Percent
|
||||
member: clamp
|
||||
---
|
||||
|
||||
Pins any <code>fixed</code> into the range [0, 1] — a health fraction or volume that can never slip out of bounds.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let hp = Percent.clamp(health)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/percent/percent-lerp.md
Normal file
22
docs/language/percent/percent-lerp.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: percent-lerp
|
||||
name: Percent.lerp
|
||||
category: percent
|
||||
kind: namespace-method
|
||||
tokens: Percent.lerp
|
||||
sig: Percent.lerp(a, b, t) -> fixed
|
||||
tip: Interpolate a..b by a clamped t.
|
||||
order: 2
|
||||
ns: Percent
|
||||
member: lerp
|
||||
---
|
||||
|
||||
Interpolates from <code>a</code> to <code>b</code> by <code>t</code>, clamping <code>t</code> to [0, 1] first so the result never overshoots the endpoints.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let x = Percent.lerp(startX, endX, progress)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/percent/percent-of.md
Normal file
22
docs/language/percent/percent-of.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: percent-of
|
||||
name: Percent.of
|
||||
category: percent
|
||||
kind: namespace-method
|
||||
tokens: Percent.of
|
||||
sig: Percent.of(num, den) -> fixed
|
||||
tip: Clamped ratio num / den.
|
||||
order: 1
|
||||
ns: Percent
|
||||
member: of
|
||||
---
|
||||
|
||||
Forms the ratio <code>num / den</code> clamped to [0, 1] (0 when <code>den</code> is 0) — a filled-fraction from a part and a whole.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let filled = Percent.of(current, maximum)
|
||||
}
|
||||
}
|
||||
```
|
||||
35
examples/library/numeric.ludic
Normal file
35
examples/library/numeric.ludic
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# numeric.ludic — the "polish" numeric types: Huge.* (idle big numbers),
|
||||
# Angle.* (auto-wrapping radians) and Percent.* (clamped [0,1]). Each assertion
|
||||
# that holds prints its number, so a full run prints:
|
||||
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
||||
# See runtime/native/numeric.ludic.
|
||||
program Numeric {
|
||||
entry {
|
||||
# --- Huge: mantissa x 10^exponent, for idle magnitudes ---
|
||||
if Huge.str(Huge.from(5)) == "5.00e0" { print(1) }
|
||||
if Huge.str(Huge.from(1500)) == "1.50e3" { print(2) }
|
||||
if Huge.str(Huge.from(2000000000)) == "2.00e9" { print(3) }
|
||||
if Huge.str(Huge.mul(Huge.from(1000000), Huge.from(1000000))) == "1.00e12" { print(4) }
|
||||
if Huge.str(Huge.add(Huge.from(1000000), Huge.from(1000000))) == "2.00e6" { print(5) }
|
||||
if Huge.str(Huge.add(Huge.from(1000000000), Huge.from(1))) == "1.00e9" { print(6) } # +1 negligible
|
||||
if Huge.cmp(Huge.from(1000000000), Huge.from(1000000)) == 1 { print(7) }
|
||||
if Huge.exp(Huge.mul(Huge.from(1000000), Huge.from(1000000))) == 12 { print(8) }
|
||||
if Huge.str(Huge.from(0 - 42)) == "-4.20e1" { print(9) }
|
||||
if Huge.str(Huge.from(0)) == "0" { print(10) }
|
||||
|
||||
# --- Angle: wrapping radians (built on the deterministic Math.*) ---
|
||||
if Math.abs(Angle.diff(Angle.from_degrees(370.0), Angle.from_degrees(10.0))) < 0.05 { print(11) } # same angle
|
||||
if Angle.sin(Angle.from_degrees(90.0)) > 0.99 { print(12) }
|
||||
if Angle.cos(Angle.from_degrees(0.0)) > 0.99 { print(13) }
|
||||
if Math.abs(Angle.lerp(Angle.from_degrees(350.0), Angle.from_degrees(10.0), 0.5)) < 0.05 { print(14) } # shortest arc crosses 0
|
||||
let w = Angle.wrap(10.0)
|
||||
if w < 3.15 and w >= 0.0 - 3.15 { print(15) }
|
||||
|
||||
# --- Percent: clamped [0, 1] ---
|
||||
if floor(Percent.clamp(1.5) * 100.0) == 100 { print(16) }
|
||||
if floor(Percent.clamp(0.0 - 0.3) * 100.0) == 0 { print(17) }
|
||||
if floor(Percent.of(3.0, 4.0) * 100.0) == 75 { print(18) }
|
||||
if floor(Percent.apply(200.0, 0.25)) == 50 { print(19) }
|
||||
if floor(Percent.lerp(0.0, 100.0, 0.5)) == 50 { print(20) }
|
||||
}
|
||||
}
|
||||
165
runtime/native/numeric.ludic
Normal file
165
runtime/native/numeric.ludic
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# ============================================================================
|
||||
# numeric.ludic — three "polish" numeric types, written in Ludic:
|
||||
# Huge.* — idle/incremental big numbers (mantissa x 10^exponent)
|
||||
# Angle.* — an auto-wrapping angle in radians
|
||||
# Percent.* — a value clamped to [0, 1]
|
||||
#
|
||||
# ludicc splices this file into any program that mentions `Huge.*`, `Angle.*`
|
||||
# or `Percent.*`. It builds on the deterministic fixed-point `Math.*` namespace
|
||||
# (spliced inline, no runtime of its own), so it works in a plain tool as well
|
||||
# as a game. The namespaces (emit_call.ludic) alias each method to the matching
|
||||
# `huge_*` / `angle_*` / `percent_*` function below.
|
||||
# ============================================================================
|
||||
|
||||
# ---- Huge: mantissa + exponent, for idle-game magnitudes -------------------
|
||||
#
|
||||
# A value is m x 10^e with the mantissa m normalized to [1, 10) (or 0 for zero);
|
||||
# e is an unbounded int, so magnitudes far past 1e308 are representable. The
|
||||
# mantissa is a Q16.16 `fixed`, giving ~4 significant digits — this is a
|
||||
# DISPLAY-SCALE number for showing 1.23e45, NOT a lockstep-exact value; keep it
|
||||
# out of the deterministic simulation and use BigInt/Decimal when exactness
|
||||
# matters.
|
||||
|
||||
# NOTE: fixed values live as inline `10.0` / `1.0` literals, not `const`s — a
|
||||
# `const` reference lowers to its raw integer value typed `int` (emit_call.ludic
|
||||
# N_CONST), which silently breaks fixed-point comparisons.
|
||||
|
||||
property Huge { m: fixed = 0.0, e: int = 0 }
|
||||
|
||||
# normalize (mantissa, exponent) so the mantissa lands in [1, 10)
|
||||
function huge_make(m: fixed, e: int) -> Huge {
|
||||
let h = new Huge
|
||||
if m == 0.0 { h.m = 0.0; h.e = 0; return h }
|
||||
var neg = false
|
||||
var mm = m
|
||||
if mm < 0.0 { neg = true; mm = 0.0 - mm }
|
||||
var ee = e
|
||||
while mm >= 10.0 { mm = mm / 10.0; ee = ee + 1 }
|
||||
while mm < 1.0 { mm = mm * 10.0; ee = ee - 1 }
|
||||
if neg { mm = 0.0 - mm }
|
||||
h.m = mm; h.e = ee
|
||||
return h
|
||||
}
|
||||
|
||||
# an int -> Huge (built via i64 so the whole int range normalizes cleanly)
|
||||
function huge_from(value: int) -> Huge {
|
||||
if value == 0 { return huge_make(0.0, 0) }
|
||||
var neg = false
|
||||
var v: long = value
|
||||
if v < 0 { neg = true; v = 0 - v }
|
||||
var e = 0
|
||||
var p: long = 1
|
||||
while p * 10 <= v { p = p * 10; e = e + 1 }
|
||||
let raw: long = (v * 65536) / p # v / 10^e in Q16.16 (in [1,10))
|
||||
let rawi: int = raw
|
||||
var m: fixed = as_fixed(rawi)
|
||||
if neg { m = 0.0 - m }
|
||||
return huge_make(m, e)
|
||||
}
|
||||
|
||||
function huge_sign(a: Huge) -> int {
|
||||
if a.m > 0.0 { return 1 }
|
||||
if a.m < 0.0 { return -1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
function huge_neg(a: Huge) -> Huge { return huge_make(0.0 - a.m, a.e) }
|
||||
function huge_mul(a: Huge, b: Huge) -> Huge {
|
||||
if a.m == 0.0 or b.m == 0.0 { return huge_make(0.0, 0) }
|
||||
return huge_make(a.m * b.m, a.e + b.e)
|
||||
}
|
||||
|
||||
function huge_add(a: Huge, b: Huge) -> Huge {
|
||||
if a.m == 0.0 { return b }
|
||||
if b.m == 0.0 { return a }
|
||||
var big = a
|
||||
var small = b
|
||||
if b.e > a.e { big = b; small = a }
|
||||
let diff = big.e - small.e
|
||||
if diff > 8 { return big } # negligible at display precision
|
||||
var sm = small.m
|
||||
var k = 0
|
||||
while k < diff { sm = sm / 10.0; k = k + 1 }
|
||||
return huge_make(big.m + sm, big.e)
|
||||
}
|
||||
|
||||
function huge_sub(a: Huge, b: Huge) -> Huge { return huge_add(a, huge_neg(b)) }
|
||||
|
||||
function huge_cmp(a: Huge, b: Huge) -> int {
|
||||
let sa = huge_sign(a)
|
||||
let sb = huge_sign(b)
|
||||
if sa != sb { if sa > sb { return 1 }; return -1 }
|
||||
if sa == 0 { return 0 }
|
||||
var c = 0
|
||||
if a.e != b.e { if a.e > b.e { c = 1 } else { c = 0 - 1 } }
|
||||
else { if a.m > b.m { c = 1 } else { if a.m < b.m { c = 0 - 1 } else { c = 0 } } }
|
||||
if sa < 0 { c = 0 - c }
|
||||
return c
|
||||
}
|
||||
|
||||
function huge_mantissa(a: Huge) -> fixed { return a.m }
|
||||
function huge_exp(a: Huge) -> int { return a.e }
|
||||
|
||||
# scientific text like "1.23e45" (two-decimal mantissa); plain "0" for zero
|
||||
function huge_str(a: Huge) -> pointer {
|
||||
if a.m == 0.0 { return "0" }
|
||||
var neg = ""
|
||||
var m = a.m
|
||||
if m < 0.0 { neg = "-"; m = 0.0 - m }
|
||||
var ip: int = floor(m) # 1..9
|
||||
let frac: fixed = m - fixed(ip)
|
||||
var dd: int = floor(frac * 100.0 + 0.5) # round the two decimals to nearest
|
||||
var ex = a.e
|
||||
if dd >= 100 { dd = 0; ip = ip + 1 } # rounding carried into the ones place
|
||||
if ip >= 10 { ip = 1; ex = ex + 1 } # ...and on into the exponent
|
||||
var ds = string(dd)
|
||||
if dd < 10 { ds = "0" + ds }
|
||||
return neg + string(ip) + "." + ds + "e" + string(ex)
|
||||
}
|
||||
|
||||
# ---- Angle: an auto-wrapping radian angle ----------------------------------
|
||||
|
||||
# wrap any radian value into the half-open range [-pi, pi). pi = 3.14159265,
|
||||
# tau = 6.28318531 (inline literals — see the note above on fixed consts).
|
||||
function angle_wrap(a: fixed) -> fixed {
|
||||
var x = a
|
||||
while x >= 3.14159265 { x = x - 6.28318531 }
|
||||
while x < 0.0 - 3.14159265 { x = x + 6.28318531 }
|
||||
return x
|
||||
}
|
||||
|
||||
function angle_from_degrees(d: fixed) -> fixed { return angle_wrap(Math.deg_to_rad(d)) }
|
||||
function angle_to_degrees(a: fixed) -> fixed { return Math.rad_to_deg(a) }
|
||||
function angle_sin(a: fixed) -> fixed { return Math.sin(a) }
|
||||
function angle_cos(a: fixed) -> fixed { return Math.cos(a) }
|
||||
function angle_add(a: fixed, b: fixed) -> fixed { return angle_wrap(a + b) }
|
||||
|
||||
# the shortest signed rotation from `a` to `b`, in [-pi, pi)
|
||||
function angle_diff(a: fixed, b: fixed) -> fixed { return angle_wrap(b - a) }
|
||||
|
||||
# interpolate from `a` toward `b` along the shortest arc (t is a fixed 0..1)
|
||||
function angle_lerp(a: fixed, b: fixed, t: fixed) -> fixed { return angle_wrap(a + angle_diff(a, b) * t) }
|
||||
|
||||
# ---- Percent: a value clamped to [0, 1] ------------------------------------
|
||||
|
||||
# clamp any fixed into [0, 1]
|
||||
function percent_clamp(v: fixed) -> fixed {
|
||||
if v < 0.0 { return 0.0 }
|
||||
if v > 1.0 { return 1.0 }
|
||||
return v
|
||||
}
|
||||
|
||||
# num / den as a clamped ratio (0 when den is 0)
|
||||
function percent_of(num: fixed, den: fixed) -> fixed {
|
||||
if den == 0.0 { return 0.0 }
|
||||
return percent_clamp(num / den)
|
||||
}
|
||||
|
||||
# linear interpolation a..b by a clamped t
|
||||
function percent_lerp(a: fixed, b: fixed, t: fixed) -> fixed {
|
||||
let p = percent_clamp(t)
|
||||
return a + (b - a) * p
|
||||
}
|
||||
|
||||
# value scaled by a clamped percent
|
||||
function percent_apply(value: fixed, p: fixed) -> fixed { return value * percent_clamp(p) }
|
||||
|
|
@ -389,6 +389,37 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "clear") { bare = "set_clear"; push(labels, "s") }
|
||||
if (meth == "members") { bare = "set_members"; push(labels, "s") }
|
||||
}
|
||||
# Huge.* / Angle.* / Percent.* -> the numeric runtime (runtime/native/numeric.ludic,
|
||||
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
|
||||
# their return types (Huge / fixed / int / bool).
|
||||
if (ns == "Huge") {
|
||||
if (meth == "from") { bare = "huge_from"; push(labels, "value") }
|
||||
if (meth == "add") { bare = "huge_add"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "sub") { bare = "huge_sub"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "mul") { bare = "huge_mul"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "neg") { bare = "huge_neg"; push(labels, "a") }
|
||||
if (meth == "cmp") { bare = "huge_cmp"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "sign") { bare = "huge_sign"; push(labels, "a") }
|
||||
if (meth == "mantissa") { bare = "huge_mantissa"; push(labels, "a") }
|
||||
if (meth == "exp") { bare = "huge_exp"; push(labels, "a") }
|
||||
if (meth == "str") { bare = "huge_str"; push(labels, "a") }
|
||||
}
|
||||
if (ns == "Angle") {
|
||||
if (meth == "from_degrees") { bare = "angle_from_degrees"; push(labels, "d") }
|
||||
if (meth == "to_degrees") { bare = "angle_to_degrees"; push(labels, "a") }
|
||||
if (meth == "wrap") { bare = "angle_wrap"; push(labels, "a") }
|
||||
if (meth == "sin") { bare = "angle_sin"; push(labels, "a") }
|
||||
if (meth == "cos") { bare = "angle_cos"; push(labels, "a") }
|
||||
if (meth == "add") { bare = "angle_add"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "diff") { bare = "angle_diff"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "lerp") { bare = "angle_lerp"; push(labels, "a"); push(labels, "b"); push(labels, "t") }
|
||||
}
|
||||
if (ns == "Percent") {
|
||||
if (meth == "clamp") { bare = "percent_clamp"; push(labels, "v") }
|
||||
if (meth == "of") { bare = "percent_of"; push(labels, "num"); push(labels, "den") }
|
||||
if (meth == "lerp") { bare = "percent_lerp"; push(labels, "a"); push(labels, "b"); push(labels, "t") }
|
||||
if (meth == "apply") { bare = "percent_apply"; push(labels, "value"); push(labels, "p") }
|
||||
}
|
||||
if (ns == "Grid") {
|
||||
if (meth == "line") { bare = "grid_line"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1") }
|
||||
if (meth == "blocked") { bare = "grid_blocked"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ function p_postfix() -> Node {
|
|||
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
|
||||
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
|
||||
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
|
||||
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand
|
||||
if e.a.kind == E_ID and e.a.s == "Query" { g_uses_query = true } # splice the ECS spatial-query runtime on demand
|
||||
if e.a.kind == E_ID and e.a.s == "Reflect" { g_uses_reflect = true } # force-emit the reflection ABI (Reflect.* reads the world schema)
|
||||
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
|
||||
|
|
@ -425,6 +426,7 @@ var cur_dir: pointer
|
|||
var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the regex runtime
|
||||
var g_uses_bignum: bool = false # a program mentioned BigInt.*/Decimal.* -> splice the bignum runtime
|
||||
var g_uses_dict: bool = false # a program mentioned Dict.*/Set.* -> splice the hash-table runtime
|
||||
var g_uses_numeric: bool = false # a program mentioned Huge.*/Angle.*/Percent.* -> splice the numeric runtime
|
||||
var g_uses_query: bool = false # a program mentioned Query.* -> splice the query runtime + reflection ABI
|
||||
var g_uses_reflect: bool = false # a program mentioned Reflect.* -> force-emit the reflection ABI
|
||||
var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D light pass
|
||||
|
|
@ -617,6 +619,13 @@ function maybe_splice_runtime() -> void {
|
|||
do_import("runtime/native/dict.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# any program that uses Huge.*/Angle.*/Percent.* gets the numeric runtime
|
||||
# spliced in (it builds on Math.*, which lowers inline, so a plain tool works).
|
||||
if g_uses_numeric {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/numeric.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# any program that uses Query.* gets the ECS spatial-query helpers spliced in;
|
||||
# they read entity state through the reflection ABI (emit_decl force-emits it
|
||||
# for a Query program even when it declares no events).
|
||||
|
|
@ -733,6 +742,7 @@ function parse_program() -> void {
|
|||
g_uses_regex = false
|
||||
g_uses_bignum = false
|
||||
g_uses_dict = false
|
||||
g_uses_numeric = false
|
||||
g_uses_query = false
|
||||
g_uses_reflect = false
|
||||
g_uses_esys = false
|
||||
|
|
|
|||
43464
selfhost/ludicc.seed.ll
43464
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -432,6 +432,34 @@
|
|||
"set-clear",
|
||||
"set-members"
|
||||
],
|
||||
"huge": [
|
||||
"huge-from",
|
||||
"huge-add",
|
||||
"huge-sub",
|
||||
"huge-mul",
|
||||
"huge-neg",
|
||||
"huge-cmp",
|
||||
"huge-sign",
|
||||
"huge-mantissa",
|
||||
"huge-exp",
|
||||
"huge-str"
|
||||
],
|
||||
"angle": [
|
||||
"angle-from_degrees",
|
||||
"angle-to_degrees",
|
||||
"angle-wrap",
|
||||
"angle-sin",
|
||||
"angle-cos",
|
||||
"angle-add",
|
||||
"angle-diff",
|
||||
"angle-lerp"
|
||||
],
|
||||
"percent": [
|
||||
"percent-clamp",
|
||||
"percent-of",
|
||||
"percent-lerp",
|
||||
"percent-apply"
|
||||
],
|
||||
"duration": [
|
||||
"duration-seconds",
|
||||
"duration-minutes",
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ function cmd_test() -> int {
|
|||
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
|
||||
feat_case("library/bignum", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "bignum.ludic (BigInt arbitrary-precision + Decimal exact base-10 money)")
|
||||
feat_case("library/containers", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "containers.ludic (Dict string-keyed hash map + Set string set)")
|
||||
feat_case("library/numeric", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "numeric.ludic (Huge idle big-numbers + Angle wrapping radians + Percent clamped [0,1])")
|
||||
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
|
||||
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
||||
feat_case("library/anim", "", "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", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue