feat(stdlib): DateTime format/parse + simulated Clock — completes #9
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Finish issue #9 by adding the two remaining acceptance items on top of the calendar/clock core, still pure-integer and deterministic: DateTime.format(dt, pattern) -> string render an instant via a token pattern (YYYY/YY/MM/DD/HH/mm/ss; other chars pass through) DateTime.parse(text, pattern) -> int read an instant back; -1 on a non-digit where one is expected Clock.now/set/advance/reset a game-controlled simulated clock (the @L_clock global) that never touches the wall clock, so gameplay reading Clock.now() is replay-safe format/parse take a string-LITERAL pattern and are expanded at compile time (field offsets are then constant), folding @fn_str_concat over literal runs and two small runtime helpers: @fn_dt_pad0 (zero-padded field) and @fn_dt_rd (fixed-width digit reader that stops at the terminator and flags malformed input). Clock is a universal i32 global declared in emit_head, so it works in entry and game programs alike. Adds examples/offline_rewards.ludic — the issue's worked "you were away N hours" example, driven from its own entry and asserted in the regression suite — plus selfhost/tests/datetime2.ludic (format/parse round-trip, parse failure, clock), docs (Clock section + 4 pages, DateTime.format/parse pages), inventory and LSP hover. Reseeded; C-free fixpoint holds; all suites green (27 self-host / 46 regression / 29 tools); check.py (366 symbols), check-impl.py (218 ns-methods) and validate.py OK. With this, #9's scope is fully delivered: DateTime/Date/Duration + core ops, format/parse, a deterministic simulated clock, docs + offline-rewards example, and tests. (v1 stays UTC-only, no leap seconds, i32 epoch valid through 2038.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1a2c6ec2c7
commit
b5455cd550
17 changed files with 13902 additions and 12278 deletions
7
docs/language/clock/_section.md
Normal file
7
docs/language/clock/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: clock
|
||||
title: Clock
|
||||
order: 6
|
||||
---
|
||||
|
||||
A game-controlled simulated clock: a single seconds counter the game owns. Unlike <code>Time.now</code> / <code>Time.since</code>, it never reads the wall clock, so any gameplay that reads <code>Clock.now()</code> is deterministic and replay-safe. Set it outright, or advance it by a <code>Duration</code> each tick to run time at whatever rate the simulation wants. The clock is a plain instant (seconds since 1970), so all the <code>DateTime.*</code> readers work on it directly.
|
||||
22
docs/language/clock/clock-advance.md
Normal file
22
docs/language/clock/clock-advance.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: clock-advance
|
||||
name: Clock.advance
|
||||
category: clock
|
||||
kind: namespace-method
|
||||
tokens: Clock.advance
|
||||
sig: Clock.advance(span)
|
||||
tip: Move the clock forward by a Duration.
|
||||
order: 2
|
||||
ns: Clock
|
||||
member: advance
|
||||
---
|
||||
|
||||
Moves the simulated clock forward by <code>span</code> seconds — pair it with the <code>Duration.*</code> constructors. Advance it by a fixed step each tick to drive a deterministic day/night or season cycle.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
Clock.advance(Duration.minutes(1)) # one in-game minute per call
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/clock/clock-now.md
Normal file
23
docs/language/clock/clock-now.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: clock-now
|
||||
name: Clock.now
|
||||
category: clock
|
||||
kind: namespace-method
|
||||
tokens: Clock.now
|
||||
sig: Clock.now() -> int
|
||||
tip: The current simulated instant.
|
||||
order: 0
|
||||
ns: Clock
|
||||
member: now
|
||||
---
|
||||
|
||||
The current value of the simulated clock, as a <code>DateTime</code> (seconds since 1970). Deterministic — it returns exactly what the game last set or advanced it to, never the wall clock.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let t = Clock.now()
|
||||
let hour = DateTime.hour(t)
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/clock/clock-reset.md
Normal file
22
docs/language/clock/clock-reset.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: clock-reset
|
||||
name: Clock.reset
|
||||
category: clock
|
||||
kind: namespace-method
|
||||
tokens: Clock.reset
|
||||
sig: Clock.reset()
|
||||
tip: Reset the clock to the epoch (0).
|
||||
order: 3
|
||||
ns: Clock
|
||||
member: reset
|
||||
---
|
||||
|
||||
Resets the simulated clock back to <code>0</code> (the 1970 epoch). Handy at the start of a new run or a replay.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
Clock.reset()
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/clock/clock-set.md
Normal file
22
docs/language/clock/clock-set.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: clock-set
|
||||
name: Clock.set
|
||||
category: clock
|
||||
kind: namespace-method
|
||||
tokens: Clock.set
|
||||
sig: Clock.set(t)
|
||||
tip: Set the clock to an instant.
|
||||
order: 1
|
||||
ns: Clock
|
||||
member: set
|
||||
---
|
||||
|
||||
Sets the simulated clock to the instant <code>t</code>. Typically called once on load (e.g. from a saved <code>Time.now()</code>), after which gameplay advances it deterministically.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
Clock.set(DateTime.from(2026, 8, 30, 8, 0, 0))
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/datetime/datetime-format.md
Normal file
23
docs/language/datetime/datetime-format.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: datetime-format
|
||||
name: DateTime.format
|
||||
category: datetime
|
||||
kind: namespace-method
|
||||
tokens: DateTime.format
|
||||
sig: DateTime.format(dt, pattern) -> string
|
||||
tip: Render an instant as text using a token pattern.
|
||||
order: 10
|
||||
ns: DateTime
|
||||
member: format
|
||||
---
|
||||
|
||||
Renders the instant <code>dt</code> as a string, filling the tokens in <code>pattern</code> with zero-padded fields and copying every other character through verbatim. The pattern <b>must be a string literal</b> — it is expanded at compile time, so there is no runtime pattern scanner. Recognised tokens: <code>YYYY</code> (4-digit year), <code>YY</code> (2-digit year), <code>MM</code> (month), <code>DD</code> (day), <code>HH</code> (hour, 24h), <code>mm</code> (minute), <code>ss</code> (second).
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let t = DateTime.from(2026, 8, 30, 7, 5, 9)
|
||||
let stamp = DateTime.format(t, "YYYY-MM-DD HH:mm:ss") # "2026-08-30 07:05:09"
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/datetime/datetime-parse.md
Normal file
25
docs/language/datetime/datetime-parse.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: datetime-parse
|
||||
name: DateTime.parse
|
||||
category: datetime
|
||||
kind: namespace-method
|
||||
tokens: DateTime.parse
|
||||
sig: DateTime.parse(text, pattern) -> int
|
||||
tip: Parse text into an instant; -1 on failure.
|
||||
order: 11
|
||||
ns: DateTime
|
||||
member: parse
|
||||
---
|
||||
|
||||
Parses <code>text</code> into a <code>DateTime</code> by reading each token of <code>pattern</code> from the fixed position the pattern lays out; missing fields default to 1970-01-01 00:00:00. Returns <code>-1</code> if a digit was expected but not found (so the caller can detect malformed input). The pattern <b>must be a string literal</b> and uses the same tokens as <code>DateTime.format</code>. <code>text</code> must match the pattern's fixed-width layout.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
handler Step phase Update {
|
||||
let t = DateTime.parse("2026-08-30 07:05:09", "YYYY-MM-DD HH:mm:ss")
|
||||
if t < 0 {
|
||||
# malformed input
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
29
examples/offline_rewards.ludic
Normal file
29
examples/offline_rewards.ludic
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# offline_rewards.ludic — issue #9's worked example: an idle/farming game grants
|
||||
# "you were away for N hours" rewards on load, using the Time/Date/Duration and
|
||||
# the deterministic Clock. Everything is integer seconds, so the result replays
|
||||
# identically — no wall clock, no floating point.
|
||||
#
|
||||
# bin/ludic examples/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0
|
||||
program OfflineRewards {
|
||||
entry {
|
||||
# A save records when the player last quit. It is hardcoded here so the demo
|
||||
# is deterministic; a real game writes Time.now() (or Clock.now()) at save.
|
||||
let last_seen = DateTime.from(2026, 8, 29, 18, 30, 0)
|
||||
|
||||
# On load, set the game clock to "now". We drive it explicitly (rather than
|
||||
# Time.now) so gameplay stays replay-safe and this sample is reproducible.
|
||||
Clock.set(DateTime.from(2026, 8, 30, 7, 45, 0))
|
||||
|
||||
let away = Clock.now() - last_seen # a Duration in seconds (47700)
|
||||
let hours = Duration.as_hours(away) # whole hours away
|
||||
|
||||
# Reward: 50 coins per full hour, capped at a day's worth.
|
||||
let capped = Math.min(hours, 24)
|
||||
let coins = capped * 50
|
||||
|
||||
print(hours) # 13
|
||||
print(coins) # 650
|
||||
print(DateTime.format(Clock.now(), "YYYY-MM-DD")) # 2026-08-30 (the day they returned)
|
||||
print(DateTime.weekday(Clock.now())) # 0 = Sunday
|
||||
}
|
||||
}
|
||||
|
|
@ -126,10 +126,132 @@ function is_datetime_ns(meth: pointer) -> bool {
|
|||
if (meth == "from") or (meth == "date") or (meth == "add") { return true }
|
||||
if (meth == "year") or (meth == "month") or (meth == "day") or (meth == "weekday") { return true }
|
||||
if (meth == "hour") or (meth == "minute") or (meth == "second") { return true }
|
||||
if (meth == "format") or (meth == "parse") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
# concat two runtime string codes -> a fresh string code
|
||||
function dt_concat(a: pointer, b: pointer) -> pointer {
|
||||
return emit_bind(`call ptr @fn_str_concat(ptr {a}, ptr {b})`)
|
||||
}
|
||||
|
||||
# does the pattern have token `tok` (length tlen) starting at index i?
|
||||
function dt_tok_at(pat: pointer, n: int, i: int, tok: pointer, tlen: int) -> bool {
|
||||
if (i + tlen) > n { return false }
|
||||
var k = 0
|
||||
while k < tlen { if pat[i + k] != tok[k] { return false }; k = k + 1 }
|
||||
return true
|
||||
}
|
||||
|
||||
# DateTime.format(t, "pattern") -> string. The pattern MUST be a string literal;
|
||||
# the tokens YYYY / YY / MM / DD / HH / mm / ss expand to zero-padded fields and
|
||||
# every other character is copied through verbatim. Expanded at compile time into
|
||||
# a fold of @fn_str_concat over literal runs and @fn_dt_pad0 field conversions.
|
||||
function emit_datetime_format(e: Node) -> Val {
|
||||
g_uses_datert = true
|
||||
g_uses_str = true
|
||||
if e.kids[1].kind != E_STR { perr("DateTime.format needs a string-literal pattern") }
|
||||
let t = emit_expr(e.kids[0])
|
||||
let pat = e.kids[1].s
|
||||
let n = len(pat)
|
||||
# compute all six components once
|
||||
let ed = dt_epochday(t.code)
|
||||
let yp = emit_alloca("i32"); let mp = emit_alloca("i32"); let dp = emit_alloca("i32")
|
||||
emit(` call void @fn_civil_from_days(i32 {ed}, ptr {yp}, ptr {mp}, ptr {dp})\n`)
|
||||
let yv = emit_bind(`load i32, ptr {yp}`)
|
||||
let mv = emit_bind(`load i32, ptr {mp}`)
|
||||
let dv = emit_bind(`load i32, ptr {dp}`)
|
||||
let sod = dt_secofday(t.code)
|
||||
let hh = emit_bind(`sdiv i32 {sod}, 3600`)
|
||||
let m3 = emit_bind(`srem i32 {sod}, 3600`)
|
||||
let mi = emit_bind(`sdiv i32 {m3}, 60`)
|
||||
let ss = emit_bind(`srem i32 {sod}, 60`)
|
||||
let yy = emit_bind(`srem i32 {yv}, 100`)
|
||||
var acc = emit_str_const("")
|
||||
let lit = buf_new()
|
||||
var i = 0
|
||||
while i < n {
|
||||
var field: pointer = null; var width = 0; var tlen = 0
|
||||
if dt_tok_at(pat, n, i, "YYYY", 4) { field = yv; width = 4; tlen = 4 }
|
||||
else { if dt_tok_at(pat, n, i, "YY", 2) { field = yy; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "MM", 2) { field = mv; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "DD", 2) { field = dv; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "HH", 2) { field = hh; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "mm", 2) { field = mi; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "ss", 2) { field = ss; width = 2; tlen = 2 } } } } } } }
|
||||
if (field == null) { buf_putc(lit, pat[i]); i = i + 1 }
|
||||
else {
|
||||
let ls = buf_str(lit)
|
||||
if len(ls) > 0 { acc = dt_concat(acc, emit_str_const(ls)) }
|
||||
let piece = emit_bind(`call ptr @fn_dt_pad0(i32 {field}, i32 {width})`)
|
||||
acc = dt_concat(acc, piece)
|
||||
lit.len = 0 # start a fresh literal run
|
||||
i = i + tlen
|
||||
}
|
||||
}
|
||||
let tail = buf_str(lit)
|
||||
if len(tail) > 0 { acc = dt_concat(acc, emit_str_const(tail)) }
|
||||
return val(acc, "string")
|
||||
}
|
||||
|
||||
# DateTime.parse(s, "pattern") -> DateTime. The pattern MUST be a string literal;
|
||||
# each field is read from a fixed offset (the layout the pattern fixes) and any
|
||||
# non-digit where a digit is expected fails the parse, returning -1. Missing
|
||||
# fields default to 1970-01-01 00:00:00.
|
||||
function emit_datetime_parse(e: Node) -> Val {
|
||||
g_uses_datert = true
|
||||
if e.kids[1].kind != E_STR { perr("DateTime.parse needs a string-literal pattern") }
|
||||
let s = emit_expr(e.kids[0])
|
||||
let pat = e.kids[1].s
|
||||
let n = len(pat)
|
||||
let failp = emit_alloca("i32")
|
||||
store_at("i32", "0", failp)
|
||||
# component slots, seeded to the epoch defaults
|
||||
let yp = emit_alloca("i32"); store_at("i32", "1970", yp)
|
||||
let mp = emit_alloca("i32"); store_at("i32", "1", mp)
|
||||
let dp = emit_alloca("i32"); store_at("i32", "1", dp)
|
||||
let hp = emit_alloca("i32"); store_at("i32", "0", hp)
|
||||
let np = emit_alloca("i32"); store_at("i32", "0", np)
|
||||
let sp = emit_alloca("i32"); store_at("i32", "0", sp)
|
||||
var off = 0
|
||||
var i = 0
|
||||
while i < n {
|
||||
var slot: pointer = null; var width = 0; var tlen = 0
|
||||
if dt_tok_at(pat, n, i, "YYYY", 4) { slot = yp; width = 4; tlen = 4 }
|
||||
else { if dt_tok_at(pat, n, i, "MM", 2) { slot = mp; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "DD", 2) { slot = dp; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "HH", 2) { slot = hp; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "mm", 2) { slot = np; width = 2; tlen = 2 }
|
||||
else { if dt_tok_at(pat, n, i, "ss", 2) { slot = sp; width = 2; tlen = 2 } } } } } }
|
||||
if (slot == null) { off = off + 1; i = i + 1 }
|
||||
else {
|
||||
let rd = emit_bind(`call i32 @fn_dt_rd(ptr {s.code}, i32 {itoa(off)}, i32 {width}, ptr {failp})`)
|
||||
store_at("i32", rd, slot)
|
||||
off = off + width
|
||||
i = i + tlen
|
||||
}
|
||||
}
|
||||
let y = emit_bind(`load i32, ptr {yp}`)
|
||||
let mo = emit_bind(`load i32, ptr {mp}`)
|
||||
let d = emit_bind(`load i32, ptr {dp}`)
|
||||
let h = emit_bind(`load i32, ptr {hp}`)
|
||||
let mn = emit_bind(`load i32, ptr {np}`)
|
||||
let sc = emit_bind(`load i32, ptr {sp}`)
|
||||
let ed = emit_bind(`call i32 @fn_days_from_civil(i32 {y}, i32 {mo}, i32 {d})`)
|
||||
let days = emit_bind(`mul i32 {ed}, 86400`)
|
||||
let hs = emit_bind(`mul i32 {h}, 3600`)
|
||||
let ns = emit_bind(`mul i32 {mn}, 60`)
|
||||
let a1 = emit_bind(`add i32 {days}, {hs}`)
|
||||
let a2 = emit_bind(`add i32 {a1}, {ns}`)
|
||||
let inst = emit_bind(`add i32 {a2}, {sc}`)
|
||||
let failed = emit_bind(`load i32, ptr {failp}`)
|
||||
let bad = emit_bind(`icmp ne i32 {failed}, 0`)
|
||||
return val(emit_bind(`select i1 {bad}, i32 -1, i32 {inst}`), "int")
|
||||
}
|
||||
|
||||
function emit_datetime_ns(meth: pointer, e: Node) -> Val {
|
||||
if (meth == "format") { return emit_datetime_format(e) }
|
||||
if (meth == "parse") { return emit_datetime_parse(e) }
|
||||
if (meth == "from") { # from(year, month, day, hour, minute, second) -> DateTime
|
||||
g_uses_datert = true
|
||||
let y = emit_expr(e.kids[0]); let mo = emit_expr(e.kids[1]); let d = emit_expr(e.kids[2])
|
||||
|
|
@ -259,4 +381,65 @@ function emit_datetime_prelude() -> void {
|
|||
emith(" store i32 %dd, ptr %dp\n")
|
||||
emith(" ret void\n")
|
||||
emith("}\n")
|
||||
# @fn_dt_pad0(v, w): a fresh, malloc'd, w-digit zero-padded decimal of v
|
||||
# (v assumed non-negative). Backs DateTime.format's numeric fields.
|
||||
emith("define ptr @fn_dt_pad0(i32 %v, i32 %w) {\n")
|
||||
emith(" %we = zext i32 %w to i64\n")
|
||||
emith(" %sz = add i64 %we, 1\n")
|
||||
emith(" %buf = call ptr @malloc(i64 %sz)\n")
|
||||
emith(" %endp = getelementptr i8, ptr %buf, i32 %w\n")
|
||||
emith(" store i8 0, ptr %endp\n")
|
||||
emith(" %kp = alloca i32\n %vp = alloca i32\n")
|
||||
emith(" %km1 = sub i32 %w, 1\n store i32 %km1, ptr %kp\n store i32 %v, ptr %vp\n")
|
||||
emith(" br label %loop\n")
|
||||
emith("loop:\n %k = load i32, ptr %kp\n %kok = icmp sge i32 %k, 0\n br i1 %kok, label %body, label %done\n")
|
||||
emith("body:\n %vv = load i32, ptr %vp\n %d = srem i32 %vv, 10\n %ch = add i32 %d, 48\n %ch8 = trunc i32 %ch to i8\n")
|
||||
emith(" %cp = getelementptr i8, ptr %buf, i32 %k\n store i8 %ch8, ptr %cp\n")
|
||||
emith(" %vn = sdiv i32 %vv, 10\n store i32 %vn, ptr %vp\n %kn = sub i32 %k, 1\n store i32 %kn, ptr %kp\n br label %loop\n")
|
||||
emith("done:\n ret ptr %buf\n}\n")
|
||||
# @fn_dt_rd(s, off, w, failp): read w decimal digits of s starting at off into an
|
||||
# int; on any non-digit set *failp = 1. Backs DateTime.parse's fixed-width fields.
|
||||
emith("define i32 @fn_dt_rd(ptr %s, i32 %off, i32 %w, ptr %failp) {\n")
|
||||
emith(" %accp = alloca i32\n store i32 0, ptr %accp\n %kp = alloca i32\n store i32 0, ptr %kp\n")
|
||||
emith(" br label %loop\n")
|
||||
emith("loop:\n %k = load i32, ptr %kp\n %kok = icmp slt i32 %k, %w\n br i1 %kok, label %body, label %done\n")
|
||||
emith("body:\n %idx = add i32 %off, %k\n %cp = getelementptr i8, ptr %s, i32 %idx\n %c8 = load i8, ptr %cp\n %c = sext i8 %c8 to i32\n")
|
||||
emith(" %zero = icmp eq i32 %c, 0\n br i1 %zero, label %stop, label %digit\n") # stop at the terminator, never read past it
|
||||
emith("stop:\n store i32 1, ptr %failp\n br label %done\n")
|
||||
emith("digit:\n %lt = icmp slt i32 %c, 48\n %gt = icmp sgt i32 %c, 57\n %isbad = or i1 %lt, %gt\n br i1 %isbad, label %fail, label %ok\n")
|
||||
emith("fail:\n store i32 1, ptr %failp\n br label %next\n")
|
||||
emith("ok:\n %acc0 = load i32, ptr %accp\n %m10 = mul i32 %acc0, 10\n %dg = sub i32 %c, 48\n %acc1 = add i32 %m10, %dg\n store i32 %acc1, ptr %accp\n br label %next\n")
|
||||
emith("next:\n %kn = add i32 %k, 1\n store i32 %kn, ptr %kp\n br label %loop\n")
|
||||
emith("done:\n %r = load i32, ptr %accp\n ret i32 %r\n}\n")
|
||||
}
|
||||
|
||||
# --- Clock.* — a game-controlled simulated clock -----------------------------
|
||||
# @L_clock (declared in emit_head, universal) is a plain seconds counter the game
|
||||
# owns: unlike Time.now / Time.since, it never touches the wall clock, so gameplay
|
||||
# that reads Clock.now() is deterministic and replay-safe. Advance it however the
|
||||
# simulation dictates (e.g. by Duration.* each tick), or set it outright.
|
||||
function is_clock_ns(meth: pointer) -> bool {
|
||||
if (meth == "now") or (meth == "set") or (meth == "advance") or (meth == "reset") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function emit_clock_ns(meth: pointer, e: Node) -> Val {
|
||||
if (meth == "now") { # the current simulated instant
|
||||
return val(emit_bind("load i32, ptr @L_clock"), "int")
|
||||
}
|
||||
if (meth == "reset") { # back to the epoch (0)
|
||||
emit(" store i32 0, ptr @L_clock\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (meth == "set") { # set the clock to instant t
|
||||
let t = emit_expr(e.kids[0])
|
||||
emit(` store i32 {t.code}, ptr @L_clock\n`)
|
||||
return val("0", "void")
|
||||
}
|
||||
# advance: move the clock forward by a Duration (seconds)
|
||||
let d = emit_expr(e.kids[0])
|
||||
let cur = emit_bind("load i32, ptr @L_clock")
|
||||
let nv = emit_bind(`add i32 {cur}, {d.code}`)
|
||||
emit(` store i32 {nv}, ptr @L_clock\n`)
|
||||
return val("0", "void")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
|
||||
perr(`unknown builtin DateTime.{meth}`)
|
||||
}
|
||||
if (ns == "Clock") {
|
||||
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
|
||||
perr(`unknown builtin Clock.{meth}`)
|
||||
}
|
||||
var bare: pointer = null
|
||||
let labels = new []pointer
|
||||
if (ns == "Screen") {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ function emit_header() -> void {
|
|||
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\0A\\00\"\n")
|
||||
emith("@L_argc = internal global i32 0\n")
|
||||
emith("@L_argv = internal global ptr null\n")
|
||||
emith("@L_clock = internal global i32 0\n") # Clock.* — the game-controlled simulated clock
|
||||
emith("@.gametitle = private unnamed_addr constant [")
|
||||
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
|
||||
emith("%LSlice = type { ptr, i32, i32 }\n")
|
||||
|
|
|
|||
25774
selfhost/ludicc.seed.ll
25774
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
23
selfhost/tests/datetime2.ludic
Normal file
23
selfhost/tests/datetime2.ludic
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
program T {
|
||||
entry {
|
||||
let t = DateTime.from(2026, 8, 30, 7, 5, 9)
|
||||
|
||||
# format — tokens expand zero-padded, other chars pass through
|
||||
print(DateTime.format(t, "YYYY-MM-DD")) # 2026-08-30
|
||||
print(DateTime.format(t, "HH:mm:ss")) # 07:05:09
|
||||
print(DateTime.format(t, "DD/MM/YY")) # 30/08/26
|
||||
|
||||
# parse — round-trips to the same instant; missing fields default to midnight
|
||||
print(DateTime.parse("2026-08-30", "YYYY-MM-DD") - Date.to_epoch(Date.new(2026, 8, 30))) # 0
|
||||
print(DateTime.parse("zz", "YYYY")) # -1 (non-digit -> parse fails)
|
||||
|
||||
# Clock — the game-controlled simulated clock
|
||||
Clock.set(3600)
|
||||
print(Clock.now()) # 3600
|
||||
Clock.advance(Duration.minutes(30))
|
||||
print(Clock.now()) # 5400
|
||||
print(DateTime.minute(Clock.now())) # 30
|
||||
Clock.reset()
|
||||
print(Clock.now()) # 0
|
||||
}
|
||||
}
|
||||
|
|
@ -400,6 +400,14 @@
|
|||
"datetime-weekday",
|
||||
"datetime-hour",
|
||||
"datetime-minute",
|
||||
"datetime-second"
|
||||
"datetime-second",
|
||||
"datetime-format",
|
||||
"datetime-parse"
|
||||
],
|
||||
"clock": [
|
||||
"clock-now",
|
||||
"clock-set",
|
||||
"clock-advance",
|
||||
"clock-reset"
|
||||
]
|
||||
}
|
||||
|
|
@ -1436,6 +1436,14 @@ program LudicLsp {
|
|||
if (meth == "hour") { return "DateTime.hour(dt) -> int" }
|
||||
if (meth == "minute") { return "DateTime.minute(dt) -> int" }
|
||||
if (meth == "second") { return "DateTime.second(dt) -> int" }
|
||||
if (meth == "format") { return "DateTime.format(dt, pattern) -> string" }
|
||||
if (meth == "parse") { return "DateTime.parse(text, pattern) -> int" }
|
||||
}
|
||||
if (ns == "Clock") {
|
||||
if (meth == "now") { return "Clock.now() -> int" }
|
||||
if (meth == "set") { return "Clock.set(t)" }
|
||||
if (meth == "advance") { return "Clock.advance(span)" }
|
||||
if (meth == "reset") { return "Clock.reset()" }
|
||||
}
|
||||
if (ns == "Memory") {
|
||||
if (meth == "bytes") { return "Memory.bytes(n) -> pointer" }
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ function cmd_selfhost_test() -> int {
|
|||
sh_case("transcend", "2718 1 148 1000 0 2303 1024 1414 3 250 0 1000 1016")
|
||||
sh_case("vector", "3 4 5 4 6 2 2 11 2 6 999 999 999 1570 5 10")
|
||||
sh_case("datetime", "10800 330 172800 3 2 0 10957 2026 8 30 4 6 1 0 1 29 28 31 2 1 365 946729815 2000 1 1 12 30 15 6 10957 1")
|
||||
sh_case("datetime2", "2026-08-30 07:05:09 30/08/26 0 -1 3600 5400 30 0")
|
||||
sh_case("textsplit", "1 1 1 3 1 1 1 1 1 1 1")
|
||||
sh_case("hash", "-2128831035 -468965076 114400290 114400290 0 -873187034 1095738169 0 1364076727 -2114883783 -845898438 -845898438 -78065325 -78057399 -3750763034362895579 -5808556873153909620 -4100651535478758590 -4100651535478758590 0 -5451962507482445012 7256831767414464289")
|
||||
sh_case("long", "1000000000000 1000000000001 1000000000005 3000000000000 1 1 1 -1000000000000 1000000 13")
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ function cmd_test() -> int {
|
|||
feat_case("detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
|
||||
feat_case("reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
|
||||
|
||||
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
|
||||
net_case("offline_rewards", "13 650 2026-08-30 0")
|
||||
|
||||
# EV0b + NETWORKING N0–N6: each a self-contained pure-Ludic program, driven and
|
||||
# asserted from its own `entry`. The transport is the compiler's built-in
|
||||
# loopback, so a networked game runs with zero foreign code.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue