improved logging

This commit is contained in:
2026-02-18 10:16:01 -06:00
parent b16fa75706
commit 76c482b84e
8 changed files with 849 additions and 22 deletions

View File

@@ -272,7 +272,9 @@ if (is_actor(some_value)) {
### log ### log
Logging functions: `log.console(msg)`, `log.error(msg)`, `log.system(msg)`. Channel-based logging. Any `log.X(value)` writes to channel `"X"`. Three channels are conventional: `log.console(msg)`, `log.error(msg)`, `log.system(msg)` — but any name works.
Channels are routed to configurable **sinks** (console or file) defined in `.cell/log.toml`. See [Logging](/docs/logging/) for the full guide.
### use(path) ### use(path)

View File

@@ -286,6 +286,84 @@ Clean build artifacts.
pit clean pit clean
``` ```
## Logging
### pit log
Manage log sinks and read log files. See [Logging](/docs/logging/) for the full guide.
### pit log list
List configured sinks.
```bash
pit log list
```
### pit log add
Add a log sink.
```bash
pit log add <name> console [options] # add a console sink
pit log add <name> file <path> [options] # add a file sink
```
Options:
- `--format=pretty|bare|json` — output format (default: `pretty` for console, `json` for file)
- `--channels=ch1,ch2` — channels to subscribe (default: `console,error,system`). Use `'*'` for all channels (quote to prevent shell glob expansion).
- `--exclude=ch1,ch2` — channels to exclude (useful with `'*'`)
```bash
pit log add terminal console --format=bare --channels=console
pit log add errors file .cell/logs/errors.jsonl --channels=error
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
```
### pit log remove
Remove a sink.
```bash
pit log remove <name>
```
### pit log read
Read entries from a file sink.
```bash
pit log read <sink> [options]
```
Options:
- `--lines=N` — show last N entries
- `--channel=X` — filter by channel
- `--since=timestamp` — only show entries after timestamp (seconds since epoch)
```bash
pit log read errors --lines=50
pit log read dump --channel=debug --lines=10
pit log read errors --since=1702656000
```
### pit log tail
Follow a file sink in real time.
```bash
pit log tail <sink> [--lines=N]
```
`--lines=N` controls how many existing entries to show on start (default: 10).
```bash
pit log tail dump
pit log tail errors --lines=20
```
## Developer Commands ## Developer Commands
Compiler pipeline tools, analysis, and testing. These are primarily useful for developing the ƿit compiler and runtime. Compiler pipeline tools, analysis, and testing. These are primarily useful for developing the ƿit compiler and runtime.

202
docs/logging.md Normal file
View File

@@ -0,0 +1,202 @@
---
title: "Logging"
description: "Configurable channel-based logging with sinks"
weight: 25
type: "docs"
---
Logging in ƿit is channel-based. Any `log.X(value)` call writes to channel `"X"`. Channels are routed to **sinks** — named destinations that format and deliver log output to the console or to files.
## Channels
Three channels are conventional:
| Channel | Usage |
|---------|-------|
| `log.console(msg)` | General output |
| `log.error(msg)` | Errors and warnings |
| `log.system(msg)` | Internal system messages |
Any name works. `log.debug(msg)` creates channel `"debug"`, `log.perf(msg)` creates `"perf"`, and so on.
```javascript
log.console("server started on port 8080")
log.error("connection refused")
log.debug({query: "SELECT *", rows: 42})
```
Non-text values are JSON-encoded automatically.
## Default Behavior
With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format:
```
[a3f12] [console] main.ce:5 server started on port 8080
[a3f12] [error] main.ce:12 connection refused
```
The format is `[actor_id] [channel] file:line message`.
## Configuration
Logging is configured in `.cell/log.toml`. Each `[sink.NAME]` section defines a sink.
```toml
[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]
[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]
[sink.everything]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]
exclude = ["console"]
```
### Sink fields
| Field | Values | Description |
|-------|--------|-------------|
| `type` | `"console"`, `"file"` | Where output goes |
| `format` | `"pretty"`, `"bare"`, `"json"` | How output is formatted |
| `channels` | array of names, or `["*"]` | Which channels this sink receives |
| `exclude` | array of names | Channels to skip (useful with `"*"`) |
| `path` | file path | Output file (file sinks only) |
### Formats
**pretty** — human-readable, one line per message. Includes actor ID, channel, source location, and message.
```
[a3f12] [console] main.ce:5 server started
```
**bare** — minimal. Actor ID and message only.
```
[a3f12] server started
```
**json** — structured JSONL (one JSON object per line). Used for file sinks and machine consumption.
```json
{"actor_id":"a3f12...","timestamp":1702656000.5,"channel":"console","event":"server started","source":{"file":"main.ce","line":5,"column":3,"function":"init"}}
```
## Log Records
Every log call produces a record:
```javascript
{
actor_id: "a3f12...", // full actor GUID
timestamp: 1702656000.5, // seconds since epoch
channel: "console", // channel name
event: "the message", // value passed to log
source: {
file: "main.ce",
line: 5,
column: 3,
function: "init"
}
}
```
File sinks write one JSON-encoded record per line. Console sinks format the record according to their format setting.
## CLI
The `pit log` command manages sinks and reads log files. See [CLI — pit log](/docs/cli/#pit-log) for the full reference.
```bash
pit log list # show sinks
pit log add terminal console --format=bare --channels=console
pit log add dump file .cell/logs/dump.jsonl --channels='*' --exclude=console
pit log remove terminal
pit log read dump --lines=20 --channel=error
pit log tail dump
```
## Examples
### Development setup
Route console output to the terminal with minimal formatting. Send everything else to a structured log file for debugging.
```toml
[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]
[sink.debug]
type = "file"
path = ".cell/logs/debug.jsonl"
channels = ["*"]
exclude = ["console"]
```
```javascript
log.console("listening on :8080") // -> terminal: [a3f12] listening on :8080
log.error("bad request") // -> debug.jsonl only
log.debug({latency: 0.042}) // -> debug.jsonl only
```
### Separate error log
Keep a dedicated error log alongside a full dump.
```toml
[sink.terminal]
type = "console"
format = "pretty"
channels = ["console", "error", "system"]
[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]
[sink.all]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]
```
### JSON console
Output structured JSON to the console for piping into other tools.
```toml
[sink.json_out]
type = "console"
format = "json"
channels = ["console", "error"]
```
```bash
pit run myapp.ce | jq '.event'
```
### Reading logs
```bash
# Last 50 error entries
pit log read errors --lines=50
# Errors since a timestamp
pit log read errors --since=1702656000
# Filter a wildcard sink to one channel
pit log read all --channel=debug --lines=10
# Follow a log file in real time
pit log tail all
```

View File

@@ -316,31 +316,19 @@ var nota = use_core('nota')
var ENETSERVICE = 0.1 var ENETSERVICE = 0.1
var REPLYTIMEOUT = 60 // seconds before replies are ignored var REPLYTIMEOUT = 60 // seconds before replies are ignored
function caller_data(depth) // --- Logging system (bootstrap phase) ---
{ // Early log: prints to console before toml/time/json are loaded.
return {file: "nofile", line: 0} // Upgraded to full sink-based system after config loads (see load_log_config below).
}
function console_rec(line, file, msg) { var log_config = null
return `[${text(_cell.id, 0, 5)}] [${file}:${line}]: ${msg}\n` var channel_sinks = {}
// time: [${time.text("mb d yyyy h:nn:ss")}] var wildcard_sinks = []
} var warned_channels = {}
function log(name, args) { function log(name, args) {
var caller = caller_data(1)
var msg = args[0] var msg = args[0]
if (msg == null) msg = ""
if (name == 'console') { os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`)
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'error') {
if (msg == null) msg = "error"
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'system') {
msg = "[SYSTEM] " + msg
os.print(console_rec(caller.line, caller.file, msg))
} else {
log.console(`unknown log type: ${name}`)
}
} }
function actor_die(err) function actor_die(err)
@@ -428,6 +416,134 @@ core_extras.native_mode = native_mode
var shop = use_core('internal/shop') var shop = use_core('internal/shop')
if (native_mode) use_core('build') if (native_mode) use_core('build')
var time = use_core('time') var time = use_core('time')
var toml = use_core('toml')
// --- Logging system (full version) ---
// Now that toml, time, fd, and json are available, upgrade the log function
// from the bootstrap version to a configurable sink-based system.
function ensure_log_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
// ensure parent dir (skip last element which is the filename)
for (i = 0; i < length(parts) - 1; i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) fd.mkdir(current)
}
}
function build_sink_routing() {
channel_sinks = {}
wildcard_sinks = []
var names = array(log_config.sink)
arrfor(names, function(name) {
var sink = log_config.sink[name]
sink._name = name
if (!is_array(sink.channels)) sink.channels = []
if (is_text(sink.exclude)) sink.exclude = [sink.exclude]
if (!is_array(sink.exclude)) sink.exclude = []
if (sink.type == "file" && sink.path) ensure_log_dir(sink.path)
arrfor(sink.channels, function(ch) {
if (ch == "*") {
wildcard_sinks[] = sink
return
}
if (!channel_sinks[ch]) channel_sinks[ch] = []
channel_sinks[ch][] = sink
})
})
}
function load_log_config() {
var log_path = null
if (shop_path) {
log_path = shop_path + '/log.toml'
if (fd.is_file(log_path)) {
log_config = toml.decode(text(fd.slurp(log_path)))
}
}
if (!log_config || !log_config.sink) {
log_config = {
sink: {
terminal: {
type: "console",
format: "pretty",
channels: ["console", "error", "system"]
}
}
}
}
build_sink_routing()
}
function pretty_format(rec) {
var aid = text(rec.actor_id, 0, 5)
var src = ""
if (rec.source && rec.source.file)
src = rec.source.file + ":" + text(rec.source.line)
var ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
return `[${aid}] [${rec.channel}] ${src} ${ev}\n`
}
function bare_format(rec) {
var aid = text(rec.actor_id, 0, 5)
var ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
return `[${aid}] ${ev}\n`
}
function sink_excluded(sink, channel) {
var excluded = false
if (!sink.exclude || length(sink.exclude) == 0) return false
arrfor(sink.exclude, function(ex) {
if (ex == channel) excluded = true
})
return excluded
}
function dispatch_to_sink(sink, rec) {
var line = null
if (sink_excluded(sink, rec.channel)) return
if (sink.type == "console") {
if (sink.format == "json")
os.print(json.encode(rec, false) + "\n")
else if (sink.format == "bare")
os.print(bare_format(rec))
else
os.print(pretty_format(rec))
} else if (sink.type == "file") {
line = json.encode(rec, false) + "\n"
fd.slurpappend(sink.path, stone(blob(line)))
}
}
load_log_config()
log = function(name, args) {
var sinks = channel_sinks[name]
var event = args[0]
if (!sinks && length(wildcard_sinks) == 0) {
if (!warned_channels[name]) {
warned_channels[name] = true
os.print(`[warn] log channel '${name}' has no sinks configured\n`)
}
return
}
var caller = caller_info(2)
var rec = {
actor_id: _cell.id,
timestamp: time.number(),
channel: name,
event: event,
source: caller
}
if (sinks) arrfor(sinks, function(sink) { dispatch_to_sink(sink, rec) })
arrfor(wildcard_sinks, function(sink) { dispatch_to_sink(sink, rec) })
}
var pronto = use_core('pronto') var pronto = use_core('pronto')
var fallback = pronto.fallback var fallback = pronto.fallback
@@ -438,6 +554,7 @@ var sequence = pronto.sequence
runtime_env.actor = actor runtime_env.actor = actor
runtime_env.log = log runtime_env.log = log
runtime_env.send = send runtime_env.send = send
runtime_env.shop_path = shop_path
runtime_env.fallback = fallback runtime_env.fallback = fallback
runtime_env.parallel = parallel runtime_env.parallel = parallel
runtime_env.race = race runtime_env.race = race

View File

@@ -584,6 +584,37 @@ JSC_CCALL(fd_slurpwrite,
return JS_NULL; return JS_NULL;
) )
JSC_CCALL(fd_slurpappend,
size_t len;
const char *data = js_get_blob_data(js, &len, argv[1]);
if (!data && len > 0)
return JS_EXCEPTION;
const char *str = JS_ToCString(js, argv[0]);
if (!str) return JS_EXCEPTION;
int fd = open(str, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) {
ret = JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return ret;
}
ssize_t written = write(fd, data, len);
close(fd);
if (written != (ssize_t)len) {
ret = JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return ret;
}
JS_FreeCString(js, str);
return JS_NULL;
)
// Helper function for recursive enumeration // Helper function for recursive enumeration
static void visit_directory(JSContext *js, JSValue *results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) { static void visit_directory(JSContext *js, JSValue *results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
if (!curr_path) return; if (!curr_path) return;
@@ -733,6 +764,7 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, read, 2), MIST_FUNC_DEF(fd, read, 2),
MIST_FUNC_DEF(fd, slurp, 1), MIST_FUNC_DEF(fd, slurp, 1),
MIST_FUNC_DEF(fd, slurpwrite, 2), MIST_FUNC_DEF(fd, slurpwrite, 2),
MIST_FUNC_DEF(fd, slurpappend, 2),
MIST_FUNC_DEF(fd, lseek, 3), MIST_FUNC_DEF(fd, lseek, 3),
MIST_FUNC_DEF(fd, getcwd, 0), MIST_FUNC_DEF(fd, getcwd, 0),
MIST_FUNC_DEF(fd, rmdir, 2), MIST_FUNC_DEF(fd, rmdir, 2),

343
log.ce Normal file
View File

@@ -0,0 +1,343 @@
// cell log - Manage and read log sinks
//
// Usage:
// cell log list List configured sinks
// cell log add <name> console [opts] Add a console sink
// cell log add <name> file <path> [opts] Add a file sink
// cell log remove <name> Remove a sink
// cell log read <sink> [opts] Read from a file sink
// cell log tail <sink> [--lines=N] Follow a file sink
var toml = use('toml')
var fd = use('fd')
var json = use('json')
var log_path = shop_path + '/log.toml'
function load_config() {
if (fd.is_file(log_path)) {
return toml.decode(text(fd.slurp(log_path)))
}
return null
}
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) fd.mkdir(current)
}
}
function save_config(config) {
ensure_dir(shop_path)
fd.slurpwrite(log_path, stone(blob(toml.encode(config))))
}
function print_help() {
log.console("Usage: cell log <command> [options]")
log.console("")
log.console("Commands:")
log.console(" list List configured sinks")
log.console(" add <name> console [opts] Add a console sink")
log.console(" add <name> file <path> [opts] Add a file sink")
log.console(" remove <name> Remove a sink")
log.console(" read <sink> [opts] Read from a file sink")
log.console(" tail <sink> [--lines=N] Follow a file sink")
log.console("")
log.console("Options for add:")
log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)")
log.console(" --channels=ch1,ch2 Channels to subscribe (default: console,error,system)")
log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)")
log.console("")
log.console("Options for read:")
log.console(" --lines=N Show last N lines (default: all)")
log.console(" --channel=X Filter by channel")
log.console(" --since=timestamp Only show entries after timestamp")
}
function parse_opt(arg, prefix) {
var full = '--' + prefix + '='
if (starts_with(arg, full))
return text(arg, length(full), length(arg))
return null
}
function format_entry(entry) {
var aid = text(entry.actor_id, 0, 5)
var src = ""
var ev = null
if (entry.source && entry.source.file)
src = entry.source.file + ":" + text(entry.source.line)
ev = is_text(entry.event) ? entry.event : json.encode(entry.event)
return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev
}
function do_list() {
var config = load_config()
var names = null
if (!config || !config.sink) {
log.console("No log sinks configured.")
log.console("Default: console pretty for console/error/system")
return
}
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)'
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
if (s.type == 'file')
log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex)
else
log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex)
})
}
function do_add() {
var name = null
var sink_type = null
var path = null
var format = null
var channels = ["console", "error", "system"]
var exclude = null
var config = null
var val = null
var i = 0
if (length(args) < 3) {
log.error("Usage: cell log add <name> console|file [path] [options]")
return
}
name = args[1]
sink_type = args[2]
if (sink_type == 'file') {
if (length(args) < 4) {
log.error("Usage: cell log add <name> file <path> [options]")
return
}
path = args[3]
format = "json"
i = 4
} else if (sink_type == 'console') {
format = "pretty"
i = 3
} else {
log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
return
}
for (i = i; i < length(args); i++) {
val = parse_opt(args[i], 'format')
if (val) { format = val; continue }
val = parse_opt(args[i], 'channels')
if (val) { channels = array(val, ','); continue }
val = parse_opt(args[i], 'exclude')
if (val) { exclude = array(val, ','); continue }
}
config = load_config()
if (!config) config = {}
if (!config.sink) config.sink = {}
config.sink[name] = {type: sink_type, format: format, channels: channels}
if (path) config.sink[name].path = path
if (exclude) config.sink[name].exclude = exclude
save_config(config)
log.console("Added sink: " + name)
}
function do_remove() {
var name = null
var config = null
if (length(args) < 2) {
log.error("Usage: cell log remove <name>")
return
}
name = args[1]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
config.sink[name] = null
save_config(config)
log.console("Removed sink: " + name)
}
function do_read() {
var name = null
var max_lines = 0
var filter_channel = null
var since = 0
var config = null
var sink = null
var content = null
var lines = null
var entries = []
var entry = null
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log read <sink_name> [options]")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { max_lines = number(val); continue }
val = parse_opt(args[i], 'channel')
if (val) { filter_channel = val; continue }
val = parse_opt(args[i], 'since')
if (val) { since = number(val); continue }
}
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only read from file sinks")
return
}
if (!fd.is_file(sink.path)) {
log.console("Log file does not exist yet: " + sink.path)
return
}
content = text(fd.slurp(sink.path))
lines = array(content, '\n')
arrfor(lines, function(line) {
var parse_fn = null
if (length(line) == 0) return
parse_fn = function() {
entry = json.decode(line)
} disruption {
entry = null
}
parse_fn()
if (!entry) return
if (filter_channel && entry.channel != filter_channel) return
if (since > 0 && entry.timestamp < since) return
entries[] = entry
})
if (max_lines > 0 && length(entries) > max_lines)
entries = array(entries, length(entries) - max_lines, length(entries))
arrfor(entries, function(e) {
log.console(format_entry(e))
})
}
function do_tail() {
var name = null
var tail_lines = 10
var config = null
var sink = null
var last_size = 0
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log tail <sink_name> [--lines=N]")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { tail_lines = number(val); continue }
}
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only tail file sinks")
return
}
if (!fd.is_file(sink.path))
log.console("Waiting for log file: " + sink.path)
function poll() {
var st = null
var poll_content = null
var poll_lines = null
var start = 0
var poll_entry = null
var old_line_count = 0
var idx = 0
var parse_fn = null
if (!fd.is_file(sink.path)) {
$delay(poll, 1)
return
}
st = fd.stat(sink.path)
if (st.size == last_size) {
$delay(poll, 1)
return
}
poll_content = text(fd.slurp(sink.path))
poll_lines = array(poll_content, '\n')
if (last_size == 0 && length(poll_lines) > tail_lines) {
start = length(poll_lines) - tail_lines
} else if (last_size > 0) {
old_line_count = length(array(text(poll_content, 0, last_size), '\n'))
start = old_line_count
}
last_size = st.size
for (idx = start; idx < length(poll_lines); idx++) {
if (length(poll_lines[idx]) == 0) continue
parse_fn = function() {
poll_entry = json.decode(poll_lines[idx])
} disruption {
poll_entry = null
}
parse_fn()
if (!poll_entry) continue
os.print(format_entry(poll_entry) + "\n")
}
$delay(poll, 1)
}
poll()
}
// Main dispatch
if (length(args) == 0) {
print_help()
} else if (args[0] == 'help' || args[0] == '-h' || args[0] == '--help') {
print_help()
} else if (args[0] == 'list') {
do_list()
} else if (args[0] == 'add') {
do_add()
} else if (args[0] == 'remove') {
do_remove()
} else if (args[0] == 'read') {
do_read()
} else if (args[0] == 'tail') {
do_tail()
} else {
log.error("Unknown command: " + args[0])
print_help()
}
$stop()

View File

@@ -8143,6 +8143,56 @@ static JSValue js_stacktrace (JSContext *ctx, JSValue this_val, int argc, JSValu
return JS_NULL; return JS_NULL;
} }
static JSValue js_caller_info (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
int depth = 0;
if (argc > 0) JS_ToInt32(ctx, &depth, argv[0]);
/* Save frame pointer — JS_GetStack clears it */
JSValue saved_frame = ctx->reg_current_frame;
uint32_t saved_pc = ctx->current_register_pc;
cJSON *stack = JS_GetStack(ctx);
/* Restore so other callers still see the frame */
ctx->reg_current_frame = saved_frame;
ctx->current_register_pc = saved_pc;
const char *fn_str = "<anonymous>";
const char *file_str = "<unknown>";
int line = 0, col = 0;
if (stack) {
int n = cJSON_GetArraySize(stack);
/* depth 0 = immediate caller of caller_info, which is frame index 1
(frame 0 is caller_info itself) */
int idx = depth + 1;
if (idx >= n) idx = n - 1;
if (idx < 0) idx = 0;
cJSON *fr = cJSON_GetArrayItem(stack, idx);
const char *v;
v = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(fr, "function"));
if (v) fn_str = v;
v = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(fr, "file"));
if (v) file_str = v;
line = (int)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(fr, "line"));
col = (int)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(fr, "column"));
}
JSGCRef obj;
JS_PushGCRef(ctx, &obj);
obj.val = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj.val, "file", JS_NewString(ctx, file_str));
JS_SetPropertyStr(ctx, obj.val, "line", JS_NewInt32(ctx, line));
JS_SetPropertyStr(ctx, obj.val, "column", JS_NewInt32(ctx, col));
JS_SetPropertyStr(ctx, obj.val, "function", JS_NewString(ctx, fn_str));
JSValue result = obj.val;
JS_PopGCRef(ctx, &obj);
if (stack) cJSON_Delete(stack);
return result;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* array function and sub-functions * array function and sub-functions
* ---------------------------------------------------------------------------- * ----------------------------------------------------------------------------
@@ -11452,6 +11502,7 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
/* I/O functions */ /* I/O functions */
js_set_global_cfunc(ctx, "print", js_print, -1); /* variadic: length < 0 means no arg limit */ js_set_global_cfunc(ctx, "print", js_print, -1); /* variadic: length < 0 means no arg limit */
js_set_global_cfunc(ctx, "stacktrace", js_stacktrace, 0); js_set_global_cfunc(ctx, "stacktrace", js_stacktrace, 0);
js_set_global_cfunc(ctx, "caller_info", js_caller_info, 1);
} }
} }

View File

@@ -7,6 +7,8 @@ sections:
url: "/docs/actors/" url: "/docs/actors/"
- title: "Requestors" - title: "Requestors"
url: "/docs/requestors/" url: "/docs/requestors/"
- title: "Logging"
url: "/docs/logging/"
- title: "Reference" - title: "Reference"
pages: pages:
- title: "Built-in Functions" - title: "Built-in Functions"