Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/unreleased/1556.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- `djs/parser`: `tokenToValue` drops its defensive `default` arm — narrowed
to a new `_ValueToken` type derived from `isValueToken`'s type guard, so
the switch is exhaustive
[#1556](https://github.com/functionalscript/functionalscript/pull/1556)
15 changes: 11 additions & 4 deletions fjs/djs/parser/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @import { OrderedMap } from '../../types/ordered_map/types.ts'
* @import { AstArray, AstConst, AstModule, AstModuleRef } from '../ast/types.ts'
* @import { TokenMetadata } from '../../js/tokenizer/types.ts'
* @import { ParseError } from './types.ts'
* @import { ParseError, _ValueToken } from './types.ts'
*/

import { error, ok } from '../../types/result/module.f.mjs'
Expand Down Expand Up @@ -302,7 +302,12 @@ const endObject = state => {
return pushValue(newState)(obj)
}

/** @type {(token: DjsToken) => AstConst} */
/**
* Only ever called on a token `isValueToken` has already confirmed carries a
* value, so the switch covers every `_ValueToken` case with no fallback arm.
*
* @type {(token: _ValueToken) => AstConst}
*/
const tokenToValue = token => {
switch (token.kind) {
case 'null': return null
Expand All @@ -312,11 +317,13 @@ const tokenToValue = token => {
case 'string': return token.value
case 'bigint': return token.value
case 'undefined': return undefined
default: return null
}
}

/** @type {(token: DjsToken) => boolean} */
/**
* @param {DjsToken} token
* @returns {token is _ValueToken}
*/
const isValueToken = token => {
switch (token.kind) {
case 'null':
Expand Down
109 changes: 109 additions & 0 deletions fjs/djs/parser/proof.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,115 @@ export const proof = {
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected token')
},
() => {
// 'export' with no 'default' before eof.
const tokenList = tokenizeString('export')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// 'const' with no name before eof.
const tokenList = tokenizeString('const')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// 'const <name>' with no '=' before eof.
const tokenList = tokenizeString('const x')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// 'const <name>' followed by a token that isn't '='.
const tokenList = tokenizeString('const x 5')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected token')
},
() => {
// 'import' with no name before eof.
const tokenList = tokenizeString('import')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// 'import' followed by a token that isn't an id.
const tokenList = tokenizeString('import 5')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected token')
},
() => {
// 'import <name>' with no 'from' before eof.
const tokenList = tokenizeString('import a')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// 'import <name> from' with no module string before eof.
const tokenList = tokenizeString('import a from')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// Array opened but eof arrives before any value/']'.
const tokenList = tokenizeString('export default [')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// Object opened but eof arrives before any key/'}'.
const tokenList = tokenizeString('export default {')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// Object key given but eof arrives before ':'.
const tokenList = tokenizeString('export default {"a"')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// Object ':' given but eof arrives before the value.
const tokenList = tokenizeString('export default {"a":')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// Object value given, followed by a token that's neither ',' nor '}'.
const tokenList = tokenizeString('export default {"a":1 2}')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected token')
},
() => {
// Object ',' given but eof arrives before the next key/'}'.
const tokenList = tokenizeString('export default {"a":1,')
const obj = parseFromTokens(tokenList)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
},
() => {
// `parseFromTokens` itself, called with no tokens at all (the
// tokenizer never produces this — it always emits at least an
// `eof` token — but the exported function's own contract must
// still handle it: the initial state is neither 'result' nor
// 'error', so it falls through to the same "unexpected end".
const obj = parseFromTokens(null)
assert(obj[0] === 'error', obj)
assertEq(obj[1].message, 'unexpected end')
assertEq(obj[1].metadata, null)
},
],
errorMetadata: [
() => {
Expand Down
9 changes: 8 additions & 1 deletion fjs/djs/parser/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
/**
* Type-level API for `fjs/djs/parser/module.f.mjs`: the `ParseError` shape
* `parseFromTokens` reports.
* `parseFromTokens` reports, and the `_ValueToken` subset `tokenToValue`
* accepts.
*
* @module
*/

import type { TokenMetadata } from '../../js/tokenizer/types.ts'
import type { DjsToken } from '../tokenizer/types.ts'

export type ParseError = {
readonly message: string,
readonly metadata: TokenMetadata | null
}

/** DJS tokens that carry a directly-usable value. */
export type _ValueToken = Extract<DjsToken, {
readonly kind: 'null' | 'false' | 'true' | 'undefined' | 'string' | 'number' | 'bigint'
}>
Loading